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
|
* Ruby
|
||||||
* SQLite3
|
* SQLite3
|
||||||
* Node.js
|
|
||||||
* Yarn
|
|
||||||
|
|
||||||
#### Installing Ruby
|
#### Installing Ruby
|
||||||
|
|
||||||
|
@ -121,31 +119,6 @@ $ sqlite3 --version
|
||||||
|
|
||||||
The program should report its 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
|
#### Installing Rails
|
||||||
|
|
||||||
To install Rails, use the `gem install` command provided by RubyGems:
|
To install Rails, use the `gem install` command provided by RubyGems:
|
||||||
|
|
|
@ -3,561 +3,274 @@
|
||||||
Working with JavaScript in Rails
|
Working with JavaScript in Rails
|
||||||
================================
|
================================
|
||||||
|
|
||||||
This guide covers the built-in Ajax/JavaScript functionality of Rails (and
|
This guide covers the options for integrating JavaScript functionality into your Rails application,
|
||||||
more); it will enable you to create rich and dynamic Ajax applications with
|
including the options you have for using external JavaScript packages and how to use Turbo with
|
||||||
ease!
|
Rails.
|
||||||
|
|
||||||
After reading this guide, you will know:
|
After reading this guide, you will know:
|
||||||
|
|
||||||
* The basics of Ajax.
|
* How to use Rails without the need for a Node.js, Yarn, or a JavaScript bundler.
|
||||||
* Unobtrusive JavaScript.
|
* How to create a new Rails application using import maps, esbuild, rollup, or webpack to bundle
|
||||||
* How Rails' built-in helpers assist you.
|
your JavaScript.
|
||||||
* How to handle Ajax on the server side.
|
* What Turbo is, and how to use it.
|
||||||
* The Turbolinks gem.
|
* How to use the Turbo HTML helpers provided by Rails.
|
||||||
* How to include your Cross-Site Request Forgery token in request headers
|
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
An Introduction to Ajax
|
Import maps
|
||||||
------------------------
|
-----------
|
||||||
|
|
||||||
In order to understand Ajax, you must first understand what a web browser does
|
[Import maps](https://github.com/rails/importmap-rails) let you import JavaScript modules using
|
||||||
normally.
|
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
|
Applications using import maps do not need [Node.js](https://nodejs.org/en/) or
|
||||||
'Go', the browser (your 'client') makes a request to the server. It parses the
|
[Yarn](https://yarnpkg.com/) to function. If you plan to use Rails with `importmap-rails` to
|
||||||
response, then fetches all associated assets, like JavaScript files,
|
manage your JavaScript dependencies, there is no need to install Node.js or Yarn.
|
||||||
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
|
When using import maps, no separate build process is required, just start your server with
|
||||||
also has the ability to update information on the page. Combining these two
|
`bin/rails server` and you are good to go.
|
||||||
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.
|
|
||||||
|
|
||||||
As an example, here's some JavaScript code that makes an Ajax request:
|
### Adding NPM Packages with importmap-rails
|
||||||
|
|
||||||
```js
|
To add new packages to your import map-powered application, run the `bin/importmap pin` command
|
||||||
fetch("/test")
|
from your terminal:
|
||||||
.then((data) => data.text())
|
|
||||||
.then((html) => {
|
```bash
|
||||||
const results = document.querySelector("#results");
|
$ bin/importmap pin react react-dom
|
||||||
results.insertAdjacentHTML("beforeend", html);
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This code fetches data from "/test", and then appends the result to the element
|
Then, import the package into `application.js` as usual:
|
||||||
with an id of `results`.
|
```javascript
|
||||||
|
import React from "react"
|
||||||
Rails provides quite a bit of built-in support for building web pages with this
|
import ReactDOM from "react-dom"
|
||||||
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>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When clicked, the link background will become red. Here's the problem: what
|
Adding NPM Packages with JavaScript Bundlers
|
||||||
happens when we have lots of JavaScript we want to execute on a click?
|
--------
|
||||||
|
|
||||||
```html
|
Import maps are the default for new Rails applications, but if you prefer traditional JavaScript
|
||||||
<a href="#" onclick="this.style.backgroundColor='#009900';this.style.color='#FFFFFF';event.preventDefault();">Paint it green</a>
|
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,
|
These bundling options each come with a simple configuration and integration with the asset
|
||||||
and turn it into a function:
|
pipeline via the [jsbundling-rails](https://github.com/rails/jsbundling-rails) gem.
|
||||||
|
|
||||||
```js
|
When using a bundling option, use `bin/dev` to start the Rails server and build JavaScript for
|
||||||
window.paintIt = function(event, backgroundColor, textColor) {
|
development.
|
||||||
event.preventDefault();
|
|
||||||
event.target.style.backgroundColor = backgroundColor;
|
### Installing Node.js and Yarn
|
||||||
if (textColor) {
|
|
||||||
event.target.style.color = textColor;
|
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
|
To install Yarn, follow the installation instructions at the
|
||||||
<a href="#" onclick="paintIt(event, '#990000')">Paint it red</a>
|
[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
|
If it says something like `1.22.0`, Yarn has been installed correctly.
|
||||||
effect?
|
|
||||||
|
|
||||||
```html
|
Choosing Between Import Maps and a JavaScript Bundler
|
||||||
<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>
|
|
||||||
```
|
|
||||||
|
|
||||||
Not very DRY, eh? We can fix this by using events instead. We'll add a `data-*`
|
When you create a new Rails application, you will need to choose between import maps and a
|
||||||
attribute to our link, and then bind a handler to the click event of every link
|
JavaScript bundling solution. Every application has different requirements, and you should
|
||||||
that has that attribute:
|
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
|
Import maps are the default option because the Rails team believes in import maps' potential for
|
||||||
function paintIt(element, backgroundColor, textColor) {
|
reducing complexity, improving developer experience, and delivering performance gains.
|
||||||
element.style.backgroundColor = backgroundColor;
|
|
||||||
if (textColor) {
|
|
||||||
element.style.color = textColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
For many applications, especially those that rely primarily on the [Hotwire](https://hotwired.dev/)
|
||||||
const links = document.querySelectorAll(
|
stack for their JavaScript needs, import maps will be the right option for the long term. You
|
||||||
"a[data-background-color]"
|
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).
|
||||||
links.forEach((element) => {
|
|
||||||
element.addEventListener("click", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const {backgroundColor, textColor} = element.dataset;
|
Other applications may still need a traditional JavaScript bundler. Requirements that indicate
|
||||||
paintIt(element, backgroundColor, textColor);
|
that you should choose a traditional bundler include:
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
```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>
|
|
||||||
```
|
|
||||||
|
|
||||||
We call this 'unobtrusive' JavaScript because we're no longer mixing our
|
* If your code requires a transpilation step, such as JSX or TypeScript.
|
||||||
JavaScript into our HTML. We've properly separated our concerns, making future
|
* If you need to use JavaScript libraries that include CSS or otherwise rely on
|
||||||
change easy. We can easily add behavior to any link by adding the data
|
[Webpack loaders](https://webpack.js.org/loaders/).
|
||||||
attribute. We can run all of our JavaScript through a minimizer and
|
* If you are absolutely sure that you need
|
||||||
concatenator. We can serve our entire JavaScript bundle on every page, which
|
[tree-shaking](https://webpack.js.org/guides/tree-shaking/).
|
||||||
means that it'll get downloaded on the first page load and then be cached on
|
* If you will install Bootstrap, Bulma, PostCSS, or Dart CSS through the
|
||||||
every page after that. Lots of little benefits really add up.
|
[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
|
Turbo lets your server deliver HTML directly as an alternative to the prevailing front-end
|
||||||
in generating HTML. Sometimes, you want to add a little Ajax to those elements,
|
frameworks that reduce the server-side of your Rails application to little more than a JSON API.
|
||||||
and Rails has got your back in those cases.
|
|
||||||
|
|
||||||
Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two
|
### Turbo Drive
|
||||||
parts: the JavaScript half and the Ruby half.
|
|
||||||
|
|
||||||
Unless you have disabled the Asset Pipeline,
|
[Turbo Drive](https://turbo.hotwired.dev/handbook/drive) speeds up page loads by avoiding full-page
|
||||||
[rails-ujs](https://github.com/rails/rails/tree/main/actionview/app/assets/javascripts)
|
teardowns and rebuilds on every navigation request. Turbo Drive is an improvement on and
|
||||||
provides the JavaScript half, and the regular Ruby view helpers add appropriate
|
replacement for Turbolinks.
|
||||||
tags to your DOM.
|
|
||||||
|
|
||||||
You can read below about the different events that are fired dealing with
|
### Turbo Frames
|
||||||
remote elements inside your application.
|
|
||||||
|
|
||||||
#### 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)
|
You can use Turbo Frames to build in-place editing without any custom JavaScript, lazy load
|
||||||
is a helper that assists with writing forms. To use Ajax for your form you can
|
content, and create server-rendered, tabbed interfaces with ease.
|
||||||
pass the `:local` option to `form_with`.
|
|
||||||
|
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
|
```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 %>
|
<% end %>
|
||||||
```
|
```
|
||||||
|
|
||||||
This will generate the following HTML:
|
### Turbo Streams
|
||||||
|
|
||||||
```html
|
[Turbo Streams](https://turbo.hotwired.dev/handbook/streams) deliver page changes as fragments of
|
||||||
<form id="new-article" action="/articles" accept-charset="UTF-8" method="post" data-remote="true">
|
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
|
||||||
</form>
|
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
|
Rails will automatically look for a `.turbo_stream.erb` view file and render that view when found.
|
||||||
than by the browser's normal submit mechanism.
|
|
||||||
|
|
||||||
You probably don't want to just sit there with a filled out `<form>`, though.
|
Turbo Stream responses can also be rendered inline in the controller action:
|
||||||
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:
|
|
||||||
|
|
||||||
```js
|
```ruby
|
||||||
window.addEventListener("load", () => {
|
def create
|
||||||
const element = document.querySelector("#new-article");
|
@post = Post.new(post_params)
|
||||||
element.addEventListener("ajax:success", (event) => {
|
|
||||||
const [_data, _status, xhr] = event.detail;
|
respond_to do |format|
|
||||||
element.insertAdjacentHTML("beforeend", xhr.responseText);
|
if @post.save
|
||||||
});
|
format.turbo_stream { render turbo_stream: turbo_stream.prepend('posts', partial: 'post') }
|
||||||
element.addEventListener("ajax:error", () => {
|
else
|
||||||
element.insertAdjacentHTML("beforeend", "<p>ERROR</p>");
|
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
|
Finally, Turbo Streams can be initiated from a model or a background job using built-in helpers.
|
||||||
start.
|
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)
|
```ruby
|
||||||
is a helper that assists with generating links. It has a `:remote` option you
|
class Post < ApplicationRecord
|
||||||
can use like this:
|
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
|
```erb
|
||||||
<%= link_to "an article", @article, remote: true %>
|
<%= turbo_stream_from "posts" %>
|
||||||
```
|
```
|
||||||
|
|
||||||
which generates
|
Replacements for Rails/UJS Functionality
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
```html
|
Rails 6 shipped with a tool called UJS that allows developers to override the method of `<a>` tags
|
||||||
<a href="/articles/1" data-remote="true">an article</a>
|
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
|
### Method
|
||||||
assume that we have a list of articles that can be deleted with just one
|
|
||||||
click. We would generate some HTML like this:
|
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
|
```erb
|
||||||
<%= link_to "Delete article", @article, remote: true, method: :delete %>
|
<%= link_to "Delete post", post_path(post), data: { turbo_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?' } %>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This generates:
|
This generates:
|
||||||
|
|
||||||
```html
|
```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
|
An alternative to changing the method of a link with `data-turbo-method` is to use Rails
|
||||||
the warning message depending on the button which was activated. In this case,
|
`button_to` helper. For accessibility reasons, actual buttons and forms are preferable for any
|
||||||
you should **not** have `data-confirm` on the form itself.
|
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
|
Adding this attribute on links will trigger the dialog on click, and adding it on forms will
|
||||||
by using the `data-disable-with` attribute. This is to prevent accidental
|
trigger it on submit. For example:
|
||||||
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:
|
|
||||||
|
|
||||||
```erb
|
```erb
|
||||||
<%= form_with(model: Article.new) do |form| %>
|
<%= link_to "Delete post", post_path(post), data: { turbo_method: "delete", turbo_confirm: "Are you sure?" } %>
|
||||||
<%= form.submit data: { disable_with: "Saving..." } %>
|
|
||||||
<% end %>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This generates a form with:
|
This generates:
|
||||||
|
|
||||||
```html
|
```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