Turbo Rails Tutorial を Rails 7.2.1 / ruby 3.3.5 / Dev Container / tailwindcss でやってみた (5)

21 minute read

前回の続き です。Dev Containertailwindcss を利用して Turbo Rails Tutorial をやっています。

今回は Turbo Drive と Turbo Frame/Stream です。

前回までソースコードは takaokouji/quote-editor:94fb9f7 にあります。

環境

  • Apple M3 (MacBook Air 13 2024)
  • macOS Sonoma 14.6.1
  • Homebrew
  • Visual Studio Code 1.93.0
    • Dev Container 機能拡張をインストール済み
  • ruby 3.3.5
    • anyenvrbenv をインストール
    • ruby 3.3.5 を global に設定済み ( rbenv global 3.3.5 )
  • rails 7.2.1 gem
    • gem install rails
  • 他のソフトウェアは Docker コンテナ上にインストール

Turbo Drive

Turbo Drive は gem を導入するだけでユーザー体験が格段にアップする仕組みです。が、実際には、導入したけどフォームが動かない / 動かなくなった、ってことでオフにされがちです。一昔前の turbolinks と同じような扱い。

でも、この章を理解することでかなりの疑問が解消され、導入するための勇気がもらえます。

rails 7.2.1 では最初から Turbo Drive が有効になっているので、コードの修正は2点だけ。画面読み込みの進捗バーの色を変えています。

app/assets/stylesheets/components/turbo_progress_bar.css

@layer components {
  .turbo-progress-bar {
    @apply bg-gradient-to-tr from-primary to-primary-rotate;
  }
}

app/assets/stylesheets/application.postcss.css に以下を追加。

@import "./components/turbo_progress_bar.css";

こんな感じで進捗バーがテーマカラーの赤色になります。

赤色の進捗バー

Turbo Frames and Turbo Stream templates

ここからがチュートリアルの本題です。

オーソドックスは CRUD ウェブアプリを JavaScript を書かずに SPA っぽくしていきます。

まずはテストの修正から。このチュートリアルはテストファーストなんですよね。すごく好きです。

差分がわかりにくくなるため、いったんコメントを削除してから変更します。新規登録と更新のテストを変えています。

  test "Creating a new quote" do
    visit quotes_path
    assert_selector "h1", text: "Quotes"

    click_on "New quote"
    # (削除) assert_selector "h1", text: "New quote"
    # 新規作成画面に遷移せず、一覧画面に新規作成のためのフォームを表示する
    fill_in "Name", with: "Capybara quote"

    # (追加) 一覧画面のままであることを確認
    assert_selector "h1", text: "Quotes"
    click_on "Create quote"

    assert_selector "h1", text: "Quotes"
    assert_text "Capybara quote"
  end

  test "Updating a quote" do
    visit quotes_path
    assert_selector "h1", text: "Quotes"

    click_on "Edit", match: :first
    # (削除) assert_selector "h1", text: "Edit quote"
    # 同様に、編集画面に遷移せず、一覧画面に編集のためのフォームを表示する
    fill_in "Name", with: "Updated quote"

    assert_selector "h1", text: "Quotes"
    click_on "Update quote"

    assert_selector "h1", text: "Quotes"
    assert_text "Updated quote"
  end

テストに失敗するので、これからアプリケーションを修正していきます。

vscode ➜ /workspaces/quote-editor (main) $ bin/rails test test/system/quotes_test.rb
(省略)
Finished in 8.909481s, 0.4490 runs/s, 0.7857 assertions/s.
4 runs, 7 assertions, 2 failures, 0 errors, 0 skips

詳細表示と削除

まずは一覧の Quote の Turbo Frame/Stream 対応。

app/views/quotes/_quote.html.erb

<%# locals: (quote:) %>
<%= turbo_frame_tag quote do %>
  <div class="quote">
    <%= link_to quote.name, quote_path(quote), data: { turbo_frame: "_top" } %>
    <div class="quote__actions">
      <%= button_to "Delete",
                    quote_path(quote),
                    method: :delete,
                    class: "btn btn--light" %>
      <%= link_to "Edit",
                  edit_quote_path(quote),
                  class: "btn btn--light" %>
    </div>
  </div>
<% end %>

修正内容はこちら。

  • turbo_frame_tag quote do ~ end で括って Turbo Frame/Stream で置き換えることができるようにする。
  • Quoteの名前をクリックすると、 data: { turbo_frame: "_top" } の指定により、全画面を詳細画面で書き換える。
  • locals magic comment を追加して必須のパラメーターを明記

こんな感じで動きます。

詳細画面

続いて、削除の Turbo Frame/Stream 対応。

app/controllers/quotes_controller.rb の destroy メソッドの修正。 format.turbo_stream を追加。

def destroy
    @quote.destroy

    respond_to do |format|
      format.html { redirect_to quotes_path, notice: "Quote was successfully destroyed." }
      format.turbo_stream
    end
  end

app/views/quotes/destroy.turbo_stream.erb の追加。

<%= turbo_stream.remove @quote %>

こんな感じで動きます。

削除

これで詳細表示と削除が Turbo Frame/Stream に対応できました。

新規作成

続いて、新規作成の Turbo Frame/Stream 対応です。

app/views/quotes/new.html.erb の修正。turbo_frame_tag @quote do ~ end でフォームを括ります。

<main class="container">
  <%= link_to sanitize("&larr; Back to quotes"), quotes_path %>

  <div class="header">
    <h1>New quote</h1>
  </div>

  <%= turbo_frame_tag @quote do %>
    <%= render "form", quote: @quote %>
  <% end %>
</main>

app/views/quotes/index.html.erb の修正。「New quote」のリンクに data: { turbo_frame: dom_id(Quote.new) パラメーターを追加して、そのレスポンスを埋め込むための <%= turbo_frame_tag Quote.new %> を追加します。さらに、新規作成した Quote を追加するために render @quotesturbo_frame_tag "quotes" do ~ end で括ります。

<main class="container">
  <div class="header">
    <h1>Quotes</h1>
    <%= link_to "New quote",
                new_quote_path,
                class: "btn btn--primary",
                data: { turbo_frame: dom_id(Quote.new) } %>
  </div>

  <%= turbo_frame_tag Quote.new %>

  <%= turbo_frame_tag "quotes" do %>
    <%= render @quotes %>
  <% end %>
</main>

app/controllers/quotes_controller.rb の create メソッドの修正。Turbo Frame/Stream の対応。

  def create
    @quote = Quote.new(quote_params)

    if @quote.save
      respond_to do |format|
        format.html { redirect_to quotes_path, notice: "Quote was successfully created." }
        format.turbo_stream
      end
    else
      render :new, status: :unprocessable_entity
    end
  end

最後は app/views/quotes/create.turbo_stream.erb の追加。先ほど修正した create メソッドのレスポンス。登録した Quote を一覧に追加して、新規作成のフォームを消しています。

<%= turbo_stream.prepend "quotes", @quote %>
<%= turbo_stream.update Quote.new, "" %>

こんな感じです。

新規作成

これで新規作成も Turbo Frame/Stream に対応できました。

編集

チュートリアルとは順番が異なってしまいますが、このタイミングで編集の Turbo Frame/Stream 対応です。単に読み飛ばしていしまっていました。失敗。

app/views/quotes/edit.html.erb の修正。フォームを turbo_frame_tag @quote do ~ end で括ります。

<main class="container">
  <%= link_to sanitize("&larr; Back to quote"), quote_path(@quote) %>

  <div class="header">
    <h1>Edit quote</h1>
  </div>

  <%= turbo_frame_tag @quote do %>
    <%= render "form", quote: @quote %>
  <% end %>
</main>

これだけです。

こんな感じで動きます。

編集

編集も新規作成もフォームには一切手を加えずに SPA っぽくなりました。これは驚異的なことで、まずは従来のページ遷移を伴う CRUD を作って、あとで SPA っぽくする、みたいなことが簡単にできます。すごくワクワクしますね。

バグ修正

ちょっとフォームを触っていたところ 2 点、バグが見つかりました。

  • Quote の名前を入力せずに Create quoteしたときのエラーメッセージに CSS のスタイルがあたっていない
  • Quote の名前フォームの CSS スタイルがあたっていない

エラーメッセージのバグの原因は @import 漏れ。

app/assets/stylesheets/application.postcss.css に以下を追加。

@import "./components/error-message.css";

名前フォームのバグは simple_formのclass cssがtailwindを使っている時に反映されない場合 - kazuhitonakayama を参考にして対応。 tailwind.config.jscontent'./config/initializers/simple_form.rb' を追加。

/* 省略 */
module.exports = {
  content: [
    './app/views/**/*.html.erb',
    './app/helpers/**/*.rb',
    './app/assets/stylesheets/**/*.css',
    './app/javascript/**/*.js',
    './config/initializers/simple_form.rb'
  ],
/* 省略 */

こんな感じ。

バグ修正

これでバグを 2 つとも潰すことができました。

ソート順の修正

次はソート順の修正です。

Quote を新規作成したときは一覧の一番上に表示されるのですが、

新規作成したときは一覧の一番上

リロードすると一覧の一番下に来てしまいます。

リロードすると一番下

そこで、一覧の順番を変えて、新しい Quote から順になるようにします。

まずはテスト test/system/quotes_test.rb を修正します。一覧の一番上に表示される Quote を @quote として扱っているため、Quote のうちで一番新しいもの = Quote.last に変えます。

  setup do
    @quote = Quote.last
  end

テストを実行して失敗することを確認したら、アプリケーションを修正します。

チュートリアルではモデルに scope を追加しているのですが、ここではコントローラーを修正します。経験上、モデルに直接 scope を書きたくないからです。scope はシンプルで共通のものだけ (a minimum and only extract the common queries there)、という方針を見たことがありますが、それはとても難しいことなので、そもそも使わない、という方針にしています。

それでは app/controllers/quotes_controller.rb の index メソッドを次のように修正します。

  def index
    @quotes = Quote.order(id: :desc)
  end

修正後はテストに成功することを確認します。

こんな感じで新規作成後にリロードしても順番が変わりません。

ソート順

これでソート順が修正できました。

キャンセル

最後に新規作成をキャンセルできるようにします。

テスト test/system/quotes_test.rb を追加して、

  test "Cancel creating" do
    visit quotes_path
    click_on "New quote"

    assert(has_field?("Name"))

    click_on "Cancel"

    assert(has_no_field?("Name"))
  end

app/views/quotes/_form.html.erb にキャンセルボタン link_to "Cancel", quotes_path, class: "btn btn--light" を追加します。

<%# locals: (quote:) %>
<%= simple_form_for quote, html: { class: "quote form" } do |f| %>
  <% if quote.errors.any? %>
    <div class="error-message">
      <%= quote.errors.full_messages.to_sentence.capitalize %>
    </div>
  <% end %>

  <%= f.input :name, input_html: { autofocus: true } %>
  <%= link_to "Cancel", quotes_path, class: "btn btn--light" %>
  <%= f.submit class: "btn btn--secondary" %>
<% end %>

こんな感じ。

キャンセル

これで新規作成をキャンセルできます。

今回のまとめ

今回は Turbo Frame/Stream を使って詳細表示、削除、新規作成、編集、キャンセルを SPA っぽくしました。

チュートリアルにも書いてありますが、JavaScript を一切書かずに SPA っぽいものができました。さらに tailwindcss を使っているため (基本的には) CSS も書かなくてもいいです。つまり、Ruby と HTML だけで SPA っぽいウェブアプリケーションが作れる、ということです。これは本当にすばらしいことです。

ソースコードは takaokouji/quote-editor においていますので、興味がある方は Watch していただけると励みになります。

協力者の募集

スモウルビー (GitHub) の開発にご協力いただける方を常に募集しています。

ご協力いただける方は、 contact@smalruby.jp までご連絡いただいてもいいですし、連絡なしで「xxx のブロックに対応しました」というPRを作成してもらってもかまいません。むしろその方が好都合です。スポンサーも募集しています

また、 拙著:小学生から楽しむ きらきらRubyプログラミング をご購入いただけるとありがたいです。スモウルビーの使い方と教え方を学ぶことができる書籍です。特に小・中学校の先生に読んでいただきたいです。
小学生から楽しむ きらきらRubyプログラミング

日本中の小・中学生が学校の授業や地域のプログラミング教室でスモウルビーを使っています。みなさんのご協力で、たくさんの子どもたちがハッピーになります。ご協力、よろしくお願いします。