mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #43957 from DavidColby/update-javascript-guide-for-rails-7
Update Working With JavaScript guide for Rails 7, scrap Node/Yarn from Getting Started guide
This commit is contained in:
commit
761a2e2552
2 changed files with 195 additions and 509 deletions
|
@ -83,8 +83,6 @@ proper prerequisites installed. These include:
|
|||
|
||||
* Ruby
|
||||
* SQLite3
|
||||
* Node.js
|
||||
* Yarn
|
||||
|
||||
#### Installing Ruby
|
||||
|
||||
|
@ -121,31 +119,6 @@ $ sqlite3 --version
|
|||
|
||||
The program should report its version.
|
||||
|
||||
#### Installing Node.js and Yarn
|
||||
|
||||
Finally, you'll need Node.js and Yarn installed to manage your application's JavaScript.
|
||||
|
||||
Find the installation instructions at the [Node.js website](https://nodejs.org/en/download/) and
|
||||
verify it's installed correctly with the following command:
|
||||
|
||||
```bash
|
||||
$ node --version
|
||||
```
|
||||
|
||||
The version of your Node.js runtime should be printed out. Make sure it's greater
|
||||
than 8.16.0.
|
||||
|
||||
To install Yarn, follow the installation
|
||||
instructions at the [Yarn website](https://classic.yarnpkg.com/en/docs/install).
|
||||
|
||||
Running this command should print out Yarn version:
|
||||
|
||||
```bash
|
||||
$ yarn --version
|
||||
```
|
||||
|
||||
If it says something like "1.22.0", Yarn has been installed correctly.
|
||||
|
||||
#### Installing Rails
|
||||
|
||||
To install Rails, use the `gem install` command provided by RubyGems:
|
||||
|
|
|
@ -3,561 +3,274 @@
|
|||
Working with JavaScript in Rails
|
||||
================================
|
||||
|
||||
This guide covers the built-in Ajax/JavaScript functionality of Rails (and
|
||||
more); it will enable you to create rich and dynamic Ajax applications with
|
||||
ease!
|
||||
This guide covers the options for integrating JavaScript functionality into your Rails application,
|
||||
including the options you have for using external JavaScript packages and how to use Turbo with
|
||||
Rails.
|
||||
|
||||
After reading this guide, you will know:
|
||||
|
||||
* The basics of Ajax.
|
||||
* Unobtrusive JavaScript.
|
||||
* How Rails' built-in helpers assist you.
|
||||
* How to handle Ajax on the server side.
|
||||
* The Turbolinks gem.
|
||||
* How to include your Cross-Site Request Forgery token in request headers
|
||||
* How to use Rails without the need for a Node.js, Yarn, or a JavaScript bundler.
|
||||
* How to create a new Rails application using import maps, esbuild, rollup, or webpack to bundle
|
||||
your JavaScript.
|
||||
* What Turbo is, and how to use it.
|
||||
* How to use the Turbo HTML helpers provided by Rails.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
An Introduction to Ajax
|
||||
------------------------
|
||||
Import maps
|
||||
-----------
|
||||
|
||||
In order to understand Ajax, you must first understand what a web browser does
|
||||
normally.
|
||||
[Import maps](https://github.com/rails/importmap-rails) let you import JavaScript modules using
|
||||
logical names that map to versioned files directly from the browser. Import maps are the default
|
||||
from Rails 7, allowing anyone to build modern JavaScript applications using most NPM packages
|
||||
without the need for transpiling or bundling.
|
||||
|
||||
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'.
|
||||
Applications using import maps do not need [Node.js](https://nodejs.org/en/) or
|
||||
[Yarn](https://yarnpkg.com/) to function. If you plan to use Rails with `importmap-rails` to
|
||||
manage your JavaScript dependencies, there is no need to install Node.js or Yarn.
|
||||
|
||||
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
|
||||
powerful technique that we call Ajax.
|
||||
When using import maps, no separate build process is required, just start your server with
|
||||
`bin/rails server` and you are good to go.
|
||||
|
||||
As an example, here's some JavaScript code that makes an Ajax request:
|
||||
### Adding NPM Packages with importmap-rails
|
||||
|
||||
```js
|
||||
fetch("/test")
|
||||
.then((data) => data.text())
|
||||
.then((html) => {
|
||||
const results = document.querySelector("#results");
|
||||
results.insertAdjacentHTML("beforeend", html);
|
||||
});
|
||||
To add new packages to your import map-powered application, run the `bin/importmap pin` command
|
||||
from your terminal:
|
||||
|
||||
```bash
|
||||
$ bin/importmap pin react react-dom
|
||||
```
|
||||
|
||||
This code fetches data from "/test", and then appends the result to the element
|
||||
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
|
||||
will show you how Rails can help you write websites in this way, but it's
|
||||
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':
|
||||
|
||||
```html
|
||||
<a href="#" onclick="this.style.backgroundColor='#990000';event.preventDefault();">Paint it red</a>
|
||||
Then, import the package into `application.js` as usual:
|
||||
```javascript
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
```
|
||||
|
||||
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?
|
||||
Adding NPM Packages with JavaScript Bundlers
|
||||
--------
|
||||
|
||||
```html
|
||||
<a href="#" onclick="this.style.backgroundColor='#009900';this.style.color='#FFFFFF';event.preventDefault();">Paint it green</a>
|
||||
Import maps are the default for new Rails applications, but if you prefer traditional JavaScript
|
||||
bundling, you can create new Rails applications with your choice of
|
||||
[esbuild](https://esbuild.github.io/), [webpack](https://webpack.js.org/), or
|
||||
[rollup.js](https://rollupjs.org/guide/en/).
|
||||
|
||||
To use a bundler instead of import maps in a new Rails application, pass the `—javascript` or `-j`
|
||||
option to `rails new`:
|
||||
```
|
||||
$ rails new my_new_app --javascript=webpack
|
||||
OR
|
||||
$ rails new my_new_app -j webpack
|
||||
```
|
||||
|
||||
Awkward, right? We could pull the function definition out of the click handler,
|
||||
and turn it into a function:
|
||||
These bundling options each come with a simple configuration and integration with the asset
|
||||
pipeline via the [jsbundling-rails](https://github.com/rails/jsbundling-rails) gem.
|
||||
|
||||
```js
|
||||
window.paintIt = function(event, backgroundColor, textColor) {
|
||||
event.preventDefault();
|
||||
event.target.style.backgroundColor = backgroundColor;
|
||||
if (textColor) {
|
||||
event.target.style.color = textColor;
|
||||
}
|
||||
}
|
||||
When using a bundling option, use `bin/dev` to start the Rails server and build JavaScript for
|
||||
development.
|
||||
|
||||
### Installing Node.js and Yarn
|
||||
|
||||
If you are using a JavaScript bundler in your Rails application, Node.js and Yarn must be
|
||||
installed.
|
||||
|
||||
Find the installation instructions at the [Node.js website](https://nodejs.org/en/download/) and
|
||||
verify it’s installed correctly with the following command:
|
||||
|
||||
```bash
|
||||
$ node --version
|
||||
```
|
||||
|
||||
And then on our page:
|
||||
The version of your Node.js runtime should be printed out. Make sure it’s greater than `8.16.0`.
|
||||
|
||||
```html
|
||||
<a href="#" onclick="paintIt(event, '#990000')">Paint it red</a>
|
||||
To install Yarn, follow the installation instructions at the
|
||||
[Yarn website](https://classic.yarnpkg.com/en/docs/install). Running this command should print out
|
||||
the Yarn version:
|
||||
|
||||
```bash
|
||||
$ yarn --version
|
||||
```
|
||||
|
||||
That's a little bit better, but what about multiple links that have the same
|
||||
effect?
|
||||
If it says something like `1.22.0`, Yarn has been installed correctly.
|
||||
|
||||
```html
|
||||
<a href="#" onclick="paintIt(event, '#990000')">Paint it red</a>
|
||||
<a href="#" onclick="paintIt(event, '#009900', '#FFFFFF')">Paint it green</a>
|
||||
<a href="#" onclick="paintIt(event, '#000099', '#FFFFFF')">Paint it blue</a>
|
||||
```
|
||||
Choosing Between Import Maps and a JavaScript Bundler
|
||||
-----------------------------------------------------
|
||||
|
||||
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:
|
||||
When you create a new Rails application, you will need to choose between import maps and a
|
||||
JavaScript bundling solution. Every application has different requirements, and you should
|
||||
consider your requirements carefully before choosing a JavaScript option, as migrating from one
|
||||
option to another may be time-consuming for large, complex applications.
|
||||
|
||||
```js
|
||||
function paintIt(element, backgroundColor, textColor) {
|
||||
element.style.backgroundColor = backgroundColor;
|
||||
if (textColor) {
|
||||
element.style.color = textColor;
|
||||
}
|
||||
}
|
||||
Import maps are the default option because the Rails team believes in import maps' potential for
|
||||
reducing complexity, improving developer experience, and delivering performance gains.
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const links = document.querySelectorAll(
|
||||
"a[data-background-color]"
|
||||
);
|
||||
links.forEach((element) => {
|
||||
element.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
For many applications, especially those that rely primarily on the [Hotwire](https://hotwired.dev/)
|
||||
stack for their JavaScript needs, import maps will be the right option for the long term. You
|
||||
can read more about the reasoning behind making import maps the default in Rails 7
|
||||
[here](https://world.hey.com/dhh/rails-7-will-have-three-great-answers-to-javascript-in-2021-8d68191b).
|
||||
|
||||
const {backgroundColor, textColor} = element.dataset;
|
||||
paintIt(element, 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>
|
||||
```
|
||||
Other applications may still need a traditional JavaScript bundler. Requirements that indicate
|
||||
that you should choose a traditional bundler include:
|
||||
|
||||
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.
|
||||
* If your code requires a transpilation step, such as JSX or TypeScript.
|
||||
* If you need to use JavaScript libraries that include CSS or otherwise rely on
|
||||
[Webpack loaders](https://webpack.js.org/loaders/).
|
||||
* If you are absolutely sure that you need
|
||||
[tree-shaking](https://webpack.js.org/guides/tree-shaking/).
|
||||
* If you will install Bootstrap, Bulma, PostCSS, or Dart CSS through the
|
||||
[cssbundling-rails gem](https://github.com/rails/cssbundling-rails). All options provided by this
|
||||
gem except Tailwind will automatically install `esbuild` for you if you do not specify a different
|
||||
option in `rails new`.
|
||||
|
||||
Built-in Helpers
|
||||
----------------
|
||||
Turbo
|
||||
-----
|
||||
|
||||
### Remote Elements
|
||||
Whether you choose import maps or a traditional bundler, Rails ships with
|
||||
[Turbo](https://turbo.hotwired.dev/) to speed up your application while dramatically reducing the
|
||||
amount of JavaScript that you will need to write.
|
||||
|
||||
Rails provides a bunch of view helper methods written in Ruby to assist you
|
||||
in generating HTML. Sometimes, you want to add a little Ajax to those elements,
|
||||
and Rails has got your back in those cases.
|
||||
Turbo lets your server deliver HTML directly as an alternative to the prevailing front-end
|
||||
frameworks that reduce the server-side of your Rails application to little more than a JSON API.
|
||||
|
||||
Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two
|
||||
parts: the JavaScript half and the Ruby half.
|
||||
### Turbo Drive
|
||||
|
||||
Unless you have disabled the Asset Pipeline,
|
||||
[rails-ujs](https://github.com/rails/rails/tree/main/actionview/app/assets/javascripts)
|
||||
provides the JavaScript half, and the regular Ruby view helpers add appropriate
|
||||
tags to your DOM.
|
||||
[Turbo Drive](https://turbo.hotwired.dev/handbook/drive) speeds up page loads by avoiding full-page
|
||||
teardowns and rebuilds on every navigation request. Turbo Drive is an improvement on and
|
||||
replacement for Turbolinks.
|
||||
|
||||
You can read below about the different events that are fired dealing with
|
||||
remote elements inside your application.
|
||||
### Turbo Frames
|
||||
|
||||
#### form_with
|
||||
[Turbo Frames](https://turbo.hotwired.dev/handbook/frames) allow predefined parts of a page to be
|
||||
updated on request, without impacting the rest of the page’s content.
|
||||
|
||||
[`form_with`](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with)
|
||||
is a helper that assists with writing forms. To use Ajax for your form you can
|
||||
pass the `:local` option to `form_with`.
|
||||
You can use Turbo Frames to build in-place editing without any custom JavaScript, lazy load
|
||||
content, and create server-rendered, tabbed interfaces with ease.
|
||||
|
||||
Rails provides HTML helpers to simplify the use of Turbo Frames through the
|
||||
[turbo-rails](https://github.com/hotwired/turbo-rails) gem.
|
||||
|
||||
Using this gem, you can add a Turbo Frame to your application with the `turbo_frame_tag` helper
|
||||
like this:
|
||||
|
||||
```erb
|
||||
<%= form_with(model: @article, id: "new-article", local: false) do |form| %>
|
||||
...
|
||||
<%= turbo_frame_tag dom_id(post) do %>
|
||||
<div>
|
||||
<%= link_to post.title, post_path(path) %>
|
||||
</div>
|
||||
<% end %>
|
||||
```
|
||||
|
||||
This will generate the following HTML:
|
||||
### Turbo Streams
|
||||
|
||||
```html
|
||||
<form id="new-article" action="/articles" accept-charset="UTF-8" method="post" data-remote="true">
|
||||
...
|
||||
</form>
|
||||
[Turbo Streams](https://turbo.hotwired.dev/handbook/streams) deliver page changes as fragments of
|
||||
HTML wrapped in self-executing `<turbo-stream>` elements. Turbo Streams allow you to broadcast
|
||||
changes made by other users over WebSockets and update pieces of a page after a form submission
|
||||
without requiring a full page load.
|
||||
|
||||
Rails provides HTML and server-side helpers to simplify the use of Turbo Streams through the
|
||||
[turbo-rails](https://github.com/hotwired/turbo-rails) gem.
|
||||
|
||||
Using this gem, you can render Turbo Streams from a controller action:
|
||||
|
||||
```ruby
|
||||
def create
|
||||
@post = Post.new(post_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @post.save
|
||||
format.turbo_stream
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Note the `data-remote="true"`. Now, the form will be submitted by Ajax rather
|
||||
than by the browser's normal submit mechanism.
|
||||
Rails will automatically look for a `.turbo_stream.erb` view file and render that view when found.
|
||||
|
||||
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:
|
||||
Turbo Stream responses can also be rendered inline in the controller action:
|
||||
|
||||
```js
|
||||
window.addEventListener("load", () => {
|
||||
const element = document.querySelector("#new-article");
|
||||
element.addEventListener("ajax:success", (event) => {
|
||||
const [_data, _status, xhr] = event.detail;
|
||||
element.insertAdjacentHTML("beforeend", xhr.responseText);
|
||||
});
|
||||
element.addEventListener("ajax:error", () => {
|
||||
element.insertAdjacentHTML("beforeend", "<p>ERROR</p>");
|
||||
});
|
||||
});
|
||||
```ruby
|
||||
def create
|
||||
@post = Post.new(post_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @post.save
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.prepend('posts', partial: 'post') }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Obviously, you'll want to be a bit more sophisticated than that, but it's a
|
||||
start.
|
||||
Finally, Turbo Streams can be initiated from a model or a background job using built-in helpers.
|
||||
These broadcasts can be used to update content via a WebSocket connection to all users, keeping
|
||||
page content fresh and bringing your application to life.
|
||||
|
||||
#### link_to
|
||||
To broadcast a Turbo Stream from a model combine a model callback like this:
|
||||
|
||||
[`link_to`](https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)
|
||||
is a helper that assists with generating links. It has a `:remote` option you
|
||||
can use like this:
|
||||
```ruby
|
||||
class Post < ApplicationRecord
|
||||
after_create_commit { broadcast_append_to('posts') }
|
||||
end
|
||||
```
|
||||
|
||||
With a WebSocket connection set up on the page that should receive the updates like this:
|
||||
|
||||
```erb
|
||||
<%= link_to "an article", @article, remote: true %>
|
||||
<%= turbo_stream_from "posts" %>
|
||||
```
|
||||
|
||||
which generates
|
||||
Replacements for Rails/UJS Functionality
|
||||
----------------------------------------
|
||||
|
||||
```html
|
||||
<a href="/articles/1" data-remote="true">an article</a>
|
||||
```
|
||||
Rails 6 shipped with a tool called UJS that allows developers to override the method of `<a>` tags
|
||||
to perform non-GET requests after a hyperlink click and to add confirmation dialogs before
|
||||
executing an action. This was the default before Rails 7, but it is now recommended to use Turbo
|
||||
instead.
|
||||
|
||||
You can bind to the same Ajax events as `form_with`. Here's an example. Let's
|
||||
assume that we have a list of articles that can be deleted with just one
|
||||
click. We would generate some HTML like this:
|
||||
### Method
|
||||
|
||||
Clicking links always results in an HTTP GET request. If your application is
|
||||
[RESTful](https://en.wikipedia.org/wiki/Representational_State_Transfer), some links are in fact
|
||||
actions that change data on the server, and should be performed with non-GET requests. This
|
||||
attribute allows marking up such links with an explicit method such as "post", "put", or "delete".
|
||||
|
||||
Turbo will scan `<a>` tags in your application for the `turbo-method` data attribute and use the
|
||||
specified method when present, overriding the default GET action.
|
||||
|
||||
For example:
|
||||
|
||||
```erb
|
||||
<%= link_to "Delete article", @article, remote: true, method: :delete %>
|
||||
```
|
||||
|
||||
and write some JavaScript like this:
|
||||
|
||||
```js
|
||||
window.addEventListener("load", () => {
|
||||
const links = document.querySelectorAll("a[data-remote]");
|
||||
links.forEach((element) => {
|
||||
element.addEventListener("ajax:success", () => {
|
||||
alert("The article was deleted.");
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### button_to
|
||||
|
||||
[`button_to`](https://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:
|
||||
|
||||
```erb
|
||||
<%= button_to "An article", @article, remote: true %>
|
||||
```
|
||||
|
||||
this generates
|
||||
|
||||
```html
|
||||
<form action="/articles/1" class="button_to" data-remote="true" method="post">
|
||||
<input type="submit" value="An article" />
|
||||
</form>
|
||||
```
|
||||
|
||||
Since it's just a `<form>`, all the information on `form_with` also applies.
|
||||
|
||||
### Customize Remote Elements
|
||||
|
||||
It is possible to customize the behavior of elements with a `data-remote`
|
||||
attribute without writing a line of JavaScript. You can specify extra `data-`
|
||||
attributes to accomplish this.
|
||||
|
||||
#### `data-method`
|
||||
|
||||
Activating hyperlinks always results in an HTTP GET request. However, if your
|
||||
application is [RESTful](https://en.wikipedia.org/wiki/Representational_State_Transfer),
|
||||
some links are in fact actions that change data on the server, and must be
|
||||
performed with non-GET requests. This attribute allows marking up such links
|
||||
with an explicit method such as "post", "put" or "delete".
|
||||
|
||||
The way it works is that, when the link is activated, it constructs a hidden form
|
||||
in the document with the "action" attribute corresponding to "href" value of the
|
||||
link, and the method corresponding to `data-method` value, and submits that form.
|
||||
|
||||
NOTE: Because submitting forms with HTTP methods other than GET and POST isn't
|
||||
widely supported across browsers, all other HTTP methods are actually sent over
|
||||
POST with the intended method indicated in the `_method` parameter. Rails
|
||||
automatically detects and compensates for this.
|
||||
|
||||
#### `data-url` and `data-params`
|
||||
|
||||
Certain elements of your page aren't actually referring to any URL, but you may want
|
||||
them to trigger Ajax calls. Specifying the `data-url` attribute along with
|
||||
the `data-remote` one will trigger an Ajax call to the given URL. You can also
|
||||
specify extra parameters through the `data-params` attribute.
|
||||
|
||||
This can be useful to trigger an action on check-boxes for instance:
|
||||
|
||||
```html
|
||||
<input type="checkbox" data-remote="true"
|
||||
data-url="/update" data-params="id=10" data-method="put">
|
||||
```
|
||||
|
||||
#### `data-type`
|
||||
|
||||
It is also possible to define the Ajax `dataType` explicitly while performing
|
||||
requests for `data-remote` elements, by way of the `data-type` attribute.
|
||||
|
||||
### Confirmations
|
||||
|
||||
You can ask for an extra confirmation of the user by adding a `data-confirm`
|
||||
attribute on links and forms. The user will be presented with a JavaScript `confirm()`
|
||||
dialog containing the attribute's text. If the user chooses to cancel, the action
|
||||
doesn't take place.
|
||||
|
||||
Adding this attribute on links will trigger the dialog on click, and adding it
|
||||
on forms will trigger it on submit. For example:
|
||||
|
||||
```erb
|
||||
<%= link_to "Dangerous zone", dangerous_zone_path,
|
||||
data: { confirm: 'Are you sure?' } %>
|
||||
<%= link_to "Delete post", post_path(post), data: { turbo_method: "delete" } %>
|
||||
```
|
||||
|
||||
This generates:
|
||||
|
||||
```html
|
||||
<a href="..." data-confirm="Are you sure?">Dangerous zone</a>
|
||||
<a data-turbo-method="delete" href="...">Delete post</a>
|
||||
```
|
||||
|
||||
The attribute is also allowed on form submit buttons. This allows you to customize
|
||||
the warning message depending on the button which was activated. In this case,
|
||||
you should **not** have `data-confirm` on the form itself.
|
||||
An alternative to changing the method of a link with `data-turbo-method` is to use Rails
|
||||
`button_to` helper. For accessibility reasons, actual buttons and forms are preferable for any
|
||||
non-GET action.
|
||||
|
||||
### Confirmations
|
||||
|
||||
### Automatic disabling
|
||||
You can ask for an extra confirmation of the user by adding a `data-turbo-confirm` attribute on
|
||||
links and forms. The user will be presented with a JavaScript `confirm()` dialog containing the
|
||||
attribute’s text. If the user chooses to cancel, the action doesn't take place.
|
||||
|
||||
It is also possible to automatically disable an input while the form is submitting
|
||||
by using the `data-disable-with` attribute. This is to prevent accidental
|
||||
double-clicks from the user, which could result in duplicate HTTP requests that
|
||||
the backend may not detect as such. The value of the attribute is the text that will
|
||||
become the new value of the button in its disabled state.
|
||||
|
||||
This also works for links with `data-method` attribute.
|
||||
|
||||
For example:
|
||||
Adding this attribute on links will trigger the dialog on click, and adding it on forms will
|
||||
trigger it on submit. For example:
|
||||
|
||||
```erb
|
||||
<%= form_with(model: Article.new) do |form| %>
|
||||
<%= form.submit data: { disable_with: "Saving..." } %>
|
||||
<% end %>
|
||||
<%= link_to "Delete post", post_path(post), data: { turbo_method: "delete", turbo_confirm: "Are you sure?" } %>
|
||||
```
|
||||
|
||||
This generates a form with:
|
||||
This generates:
|
||||
|
||||
```html
|
||||
<input data-disable-with="Saving..." type="submit">
|
||||
<a href="..." data-confirm="Are you sure?" data-turbo-method="delete">Delete post</a>
|
||||
```
|
||||
|
||||
### Rails-ujs event handlers
|
||||
|
||||
Rails 5.1 introduced rails-ujs and dropped jQuery as a dependency.
|
||||
As a result the Unobtrusive JavaScript (UJS) driver has been rewritten to operate without jQuery.
|
||||
These introductions cause small changes to `custom events` fired during the request:
|
||||
|
||||
NOTE: Signature of calls to UJS's event handlers has changed.
|
||||
Unlike the version with jQuery, all custom events return only one parameter: `event`.
|
||||
In this parameter, there is an additional attribute `detail` which contains an array of extra parameters.
|
||||
For information about the previously used `jquery-ujs` in Rails 5 and earlier, read the [`jquery-ujs` wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
|
||||
|
||||
| Event name | Extra parameters (event.detail) | Fired |
|
||||
|---------------------|---------------------------------|-------------------------------------------------------------|
|
||||
| `ajax:before` | | Before the whole ajax business. |
|
||||
| `ajax:beforeSend` | [xhr, options] | Before the request is sent. |
|
||||
| `ajax:send` | [xhr] | When the request is sent. |
|
||||
| `ajax:stopped` | | When the request is stopped. |
|
||||
| `ajax:success` | [response, status, xhr] | After completion, if the response was a success. |
|
||||
| `ajax:error` | [response, status, xhr] | After completion, if the response was an error. |
|
||||
| `ajax:complete` | [xhr, status] | After the request has been completed, no matter the outcome.|
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
document.body.addEventListener("ajax:success", (event) => {
|
||||
const [data, status, xhr] = event.detail;
|
||||
});
|
||||
```
|
||||
|
||||
### Stoppable events
|
||||
|
||||
You can stop execution of the Ajax request by running `event.preventDefault()`
|
||||
from the handlers methods `ajax:before` or `ajax:beforeSend`.
|
||||
The `ajax:before` event can manipulate form data before serialization and the
|
||||
`ajax:beforeSend` event is useful for adding custom request headers.
|
||||
|
||||
If you stop the `ajax:aborted:file` event, the default behavior of allowing the
|
||||
browser to submit the form via normal means (i.e. non-Ajax submission) will be
|
||||
canceled, and the form will not be submitted at all. This is useful for
|
||||
implementing your own Ajax file upload workaround.
|
||||
|
||||
Note, you should use `return false` to prevent an event for `jquery-ujs` and
|
||||
`event.preventDefault()` for `rails-ujs`.
|
||||
|
||||
Server-Side Concerns
|
||||
--------------------
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
```ruby
|
||||
class UsersController < ApplicationController
|
||||
def index
|
||||
@users = User.all
|
||||
@user = User.new
|
||||
end
|
||||
# ...
|
||||
```
|
||||
|
||||
The index view (`app/views/users/index.html.erb`) contains:
|
||||
|
||||
```erb
|
||||
<b>Users</b>
|
||||
|
||||
<ul id="users">
|
||||
<%= render @users %>
|
||||
</ul>
|
||||
|
||||
<br>
|
||||
|
||||
<%= form_with model: @user, local: false do |form| %>
|
||||
<%= form.label :name %><br>
|
||||
<%= form.text_field :name %>
|
||||
<%= form.submit %>
|
||||
<% end %>
|
||||
```
|
||||
|
||||
The `app/views/users/_user.html.erb` partial contains the following:
|
||||
|
||||
```erb
|
||||
<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.
|
||||
|
||||
The bottom form will call the `create` action on the `UsersController`. Because
|
||||
the form's remote option is set to true, the request will be posted to the
|
||||
`UsersController` as an Ajax request, looking for JavaScript. In order to
|
||||
serve that request, the `create` action of your controller would look like
|
||||
this:
|
||||
|
||||
```ruby
|
||||
# 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
|
||||
```
|
||||
|
||||
Notice the `format.js` in the `respond_to` block: that allows the controller to
|
||||
respond to your Ajax request. You then have a corresponding
|
||||
`app/views/users/create.js.erb` view file that generates the actual JavaScript
|
||||
code that will be sent and executed on the client side.
|
||||
|
||||
```js
|
||||
var users = document.querySelector("#users");
|
||||
users.insertAdjacentHTML("beforeend", "<%= j render(@user) %>");
|
||||
```
|
||||
|
||||
NOTE: JavaScript view rendering doesn't do any preprocessing, so you shouldn't use ES6 syntax here.
|
||||
|
||||
Turbolinks
|
||||
----------
|
||||
|
||||
Rails ships with the [Turbolinks library](https://github.com/turbolinks/turbolinks),
|
||||
which uses Ajax to speed up page rendering in most applications.
|
||||
|
||||
### How Turbolinks Works
|
||||
|
||||
Turbolinks attaches a click handler to all `<a>` tags on the page. If your browser
|
||||
supports
|
||||
[PushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState%28%29_method),
|
||||
Turbolinks will make an Ajax request for the page, parse the response, and
|
||||
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.
|
||||
|
||||
If you want to disable Turbolinks for certain links, add a `data-turbolinks="false"`
|
||||
attribute to the tag:
|
||||
|
||||
```html
|
||||
<a href="..." data-turbolinks="false">No turbolinks here</a>.
|
||||
```
|
||||
|
||||
### Page Change Events
|
||||
|
||||
You'll often want to do some sort of processing upon
|
||||
page load. Using the DOM, you'd write something like this:
|
||||
|
||||
```js
|
||||
window.addEventListener("load", () => {
|
||||
alert("page has loaded!");
|
||||
});
|
||||
```
|
||||
|
||||
However, because Turbolinks overrides the normal page loading process, the
|
||||
event that this relies upon will not be fired. If you have code that looks like
|
||||
this, you must change your code to do this instead:
|
||||
|
||||
```js
|
||||
document.addEventListener("turbolinks:load", () => {
|
||||
alert("page has loaded!");
|
||||
});
|
||||
```
|
||||
|
||||
For more details, including other events you can bind to, check out [the
|
||||
Turbolinks
|
||||
README](https://github.com/turbolinks/turbolinks/blob/master/README.md).
|
||||
|
||||
Cross-Site Request Forgery (CSRF) token in Ajax
|
||||
----
|
||||
|
||||
When using another library to make Ajax calls, it is necessary to add
|
||||
the security token as a default header for Ajax calls in your library. To get
|
||||
the token:
|
||||
|
||||
```js
|
||||
const token = document.getElementsByName(
|
||||
"csrf-token"
|
||||
)[0].content;
|
||||
```
|
||||
|
||||
You can then submit this token as a `X-CSRF-Token` header for your
|
||||
Ajax request. You do not need to add a CSRF token for GET requests,
|
||||
only non-GET ones.
|
||||
|
||||
You can read more about Cross-Site Request Forgery in the [Security guide](https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf).
|
||||
|
||||
Other Resources
|
||||
---------------
|
||||
|
||||
Here are some helpful links to help you learn even more:
|
||||
|
||||
* [rails-ujs wiki](https://github.com/rails/rails/tree/main/actionview/app/assets/javascripts)
|
||||
* [Railscasts: Unobtrusive JavaScript](http://railscasts.com/episodes/205-unobtrusive-javascript)
|
||||
* [Railscasts: Turbolinks](http://railscasts.com/episodes/390-turbolinks)
|
||||
|
|
Loading…
Reference in a new issue