RubyMine

RubyMine で Turbo を Rails アプリに使用する

Read this post in other languages:

皆さん、こんにちは!

RubyMine チームは Ruby と Rails の新しいテクノロジーのサポートを提供するための継続的な取り組みを行っています。 Rails に最近追加された最も画期的な手法の 1 つは間違いなく Hotwire だと言えます。そこで、このフレームワーク一式の概要と RubyMine で最も重要な Turbo 機能と Stimulus 機能を Rails アプリに使用するためのチュートリアルを用意しました。 この記事では Turbo を取り上げています。YouTube(英語)でもご覧いいただけます。

なお、Stimulus のサポートについての詳細は、次のブログ記事をお待ちください。

Hotwire と Turbo

Hotwire とは?

Hotwire は JSON の代わりに HTML をワイヤー越しに送信することでウェブ開発を単純化します(名称は「HTML over the wire」の略称です)。 これにより、JavaScript の記述量とブラウザーへの送信量が減り、テンプレートのレンダリングをサーバー上に維持することができます。 HotwireTurboStimulus、および Strada という複数のフレームワークで構成されます。 この記事では、Turbo について説明します。

Turbo とは?

TurboJavaScript をまったく書くことなくページに部分的な更新を動的に反映するための単純な手段を提供するものです。 また、Turbo Drive により、アプリ内でのすべてのリンククリックとフォーム送信を高速化します。 このチュートリアルでは、ページ全体を再読み込みせずにリアルタイムに更新できる個別のコンテキストにページを分割することに焦点を当てます。 Turbo の他の 2 つの主要概念である Turbo StreamTurbo Frame をこの目的で使用します。

Turbo Frame とは?

Turbo Frame を使用すると、ページを独立したコンテキストに分解できます。 ドキュメントオブジェクトモジュール(DOM)の一部を Turbo Frame で囲むと、ページの他の部分を再読み込みせずにそのフレームだけを更新できます。

Turbo Stream とは?

Turbo Stream を使用すると、ページ全体を再読み込みせずに DOM の一部を更新できます。 サーバーはページ全体の HTML の代わりに Turbo Stream メッセージを送信する場合があります。このメッセージには HTML、ターゲット要素 ID、および適用されるアクションが含まれます(ターゲットへの追加、ターゲットからの削除、ターゲットの更新など。詳細については、ドキュメントをご覧ください)。 Turbo Frame と同様に、Turbo Stream でもリアルタイムにページを部分的に変更できます。 ある意味、Turbo Stream の機能のほうが強力です。対応するフレーム以外にも DOM の複数の部分を変更できるだけでなく、要素の追加や削除などのさまざまな更新を実行できます。

turbo-rails gem は Rails 7 にデフォルトで同梱されているため、アプリケーションですぐに使用し始めることができます!

Ruby および Rails 開発者用の IDE である RubyMine はコード補完や移動操作などの Turbo に対応したコードインサイトのサポートを提供していますが、それらはこのチュートリアルを進めながら使用することをお勧めします。

チュートリアル: RubyMine で Turbo を Rails アプリに使用する

このチュートリアルでは、Turbo の基本的なビルディングブロックを使用する方法を説明します。 ユーザーがアカウントを作成し、ミニ投稿を作成し、相互フォローを行い、フィードでミニ投稿を読むことのできるサンプル Rails アプリケーションを使用します。

サンプル Rails アプリをクローンする

以下のステップに従ってサンプルアプリをクローンし、動作させてください。

ステップ 1. Check out a project from a remote host (git clone)(リモートホストからプロジェクトをチェックアウトする(git clone))の説明に従ってサンプルアプリケーションをチェックアウトします。 URL フィールドに次のアドレスを入力します: https://github.com/JetBrains/sample_rails_app_7th_ed.git

ブランチは hotwire_setup をチェックアウトします。

ステップ 2. Ruby インタープリターを指定して gem をインストールします。

サンプルアプリの Alice のフィード

Turbo Frame の使用方法

このアプリでは現在、投稿を削除できます。 では、編集も追加しましょう。

  1. ファイル _micropost.html.erb を開き、タイムスタンプの「Delete(削除)」リンクの横に「Edit(編集)」リンクを追加します。



  Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  <% if current_user?(micropost.user) %>
    <%= link_to "delete", micropost, data: { "turbo-method": :delete,
                                            turbo_confirm: "You sure?" } %>
    <%= link_to "edit", edit_micropost_path(micropost) %>
  <% end %>

2. micropost controller に edit メソッドと update メソッドを追加します(なお routes.rb は事前に更新済みです)。

// micropost_controller.rb

def edit
  @micropost = Micropost.find(params[:id])
end

def update
  @micropost = Micropost.find(params[:id])
  if @micropost.update(micropost_params)
    redirect_to root_url
  else
    render :edit, status: :unprocessable_entity
  end
end

edit ボタンを押すと micropost/[id]/edit ルートにリダイレクトされ、ビューテンプレートが読み込まれます。 def edit の横のガターアイコンをクリックし、テンプレートファイル edit.html.erb を作成します。

ガターアイコンをクリックし、コントローラーアクションの横にビューテンプレートを作成します。

3. 次のコードを microposts/edit.html.erb ファイルに追加します。



<%= render 'shared/micropost_form'%>

投稿を編集するには、既存の部分的な shared/micropost_form をレンダリングする必要があります。これはすでにミニ投稿の作成に使用されているものです。 ここで edit をクリックすると、新しいページにリダイレクトされます。

Edit ボタンで新しいページに移動する

ただし、ここで行いたいのはインプレース編集です。新しいページを読み込む代わりに、既存の micropost の本文をフォームに置き換え、テキストを更新して保存しましょう。これを同じページ内で行います。

これを行うには、Turbo Frame を使用できます。

Turbo Frame を使用すると、micropost の代わりにフォームを読み込み、そのコンテンツを更新し、ページの他の部分を再読み込みすることなく更新済みのミニ投稿をレンダリングできます。

4. turbo_frame_tag ビューヘルパーを _micropost.html.erb に追加します。

<!--_micropost.html.erb-->
<%= turbo_frame_tag micropost do %>
<li id="micropost-<%= micropost.id %>">
# このタグ部分は元の内容を使用
</li>
<% end %>

このフレームを使用すると、サーバーがリクエストを処理する際に HTML のどの箇所を置き換えるべきかを指定できます。 フレームには turbo_frame_tag への引数として渡される一意の ID が割り当てられます。 この ID は文字列または文字の場合がありますが、ここで適切なフレームを確実に操作するには一意の ID が必要です。そのため、特定のレコードに依存する必要があります。 幸い、Turbo Frame は非常に使いやすくなっています。micropost オブジェクトを渡すだけで turbo_frame_tag によって dom_id ヘルパーがオブジェクトに適用され、その dom_id をフレーム ID として使用できます。

ミニ投稿をフォームに置き換えて編集できるようにするため、edit.html.erb に対応する Turbo Frame を含めるようにする必要があります。

5. microposts/edit.html.erbturbo_frame_tag ヘルパーを追加します。



<%= link_to "Back", root_path %>
<%= turbo_frame_tag @micropost do %>
  <%= render "shared/micropost_form", micropost: @micropost %>
<% end %>

turbo_frame_tag コード編集ヘルパーに渡されるフレーム ID は両方のページで一致する必要があることに注意してください。

ここで Edit リンクをクリックすると、コントローラーが Turbo Frame を使って microposts/[id]/edit リクエストに応答します。 この新しい Turbo Frame の内容は古いコンテンツを置き換えるため、ミニ投稿のコンテンツではなくミニ投稿のフォームが表示され、そのテキストを編集できるようになります。 Post(投稿)をクリックして microposts/[id]/update ルートにリクエストを送信すると、コントローラーが Turbo Frame を含む応答を再送信します。Turbo は一致するフレームを見つけ、そのコンテンツを使用して既存のコンテンツを置き換えます。ここでは更新後のミニ投稿に置き換えられます。 コンソールを見ると、Rails がリクエストをどのように処理するのかがわかります。

Turbo による編集

Turbo Frame 内のすべてのリンクまたはフォームはインターセプトされ、その親フレームまたはそのターゲットと同じ ID の Turbo Frame を見つけようとします(リンクは異なるフレームをターゲットにできます。その場合は、data: { turbo_frame: [frame id] }link_to の引数として追加します)。

では、リンクを 1 回クリックしてページ上の複数の要素を更新するにはどうすればよいのでしょうか? 要素を置き換えずに追加または削除するには? ここで役に立つのが Turbo Stream です。

Turbo Stream の使用方法

Turbo Stream の能力を示すため、それぞれのミニ投稿に unfollow リンクを追加し、現在のユーザーがフィードから他のユーザーをフォロー解除したときにリアルタイムで user.following の数を更新してみましょう。 また、フォロー解除したユーザーの投稿をすべて削除します。

  1. _micropost.html.erb を開いて、現在のユーザーによるものではない各ミニ投稿に unfollow リンクを追加します。


<% if current_user?(micropost.user) %>
  <%= link_to "delete", micropost, data: { "turbo-method": :delete,
                                           turbo_confirm: "You sure?" } %>
  <%= link_to "edit", edit_micropost_path(micropost) %>
<% elsif !current_user.nil? && current_user.following?(micropost.user) %>
  <%= link_to "unfollow",
  relationship_path(current_user.active_relationships.find_by(followed: micropost.user), user_to_update: current_user),
  data: {turbo_method: :delete, turbo_confirm: 'Are you sure?'} %>
<% end %>

このパスはどこから来たものでしょうか? 現在のアプリはユーザーが相互にフォローおよびフォロー解除できる機能をすでに実装しています。さらに、この機能は Turbo Stream も使用しています。 Turbo の理解を深めるため、その実装をレベルアップし、ユーザーがユーザーページからだけでなく、フィードからもフォロー解除できるようにしましょう。

User モデルはユーザーがフォロー中のすべてのユーザーのリスト (active_relationships) とそのユーザーをフォロー中の全ユーザーのリスト (passive_relationship) を維持しています。 ユーザーをフォロー解除するには、この 2 人のユーザーの間の active_relationships の関連付けを破棄する必要があります。そこで、適切なパスを取得して DELETE メソッドを使用するリンクを設定する必要があります(Turbo により、初期状態でこれを達成できます)。

unfollow をクリックすると何が起きるのかを見てみましょう。

Unfollow: 最初の実装

今フォロー解除したユーザーの投稿がフィードに残っているだけでなく、動的であるにもかかわらずカウンターの更新が誤っています。

これを修正するため、まずは関係を管理するコントローラーを確認しましょう。

2. ファイル relationships_controller.rb を開きます。

現在のコードは以下のようになっています。

# relationships_controller.rb

@user = Relationship.find(params[:id]).followed
current_user.unfollow(@user)
respond_to do |format|
  format.html { redirect_to @user, status: :see_other }
  format.turbo_stream
end

このコードは同じアプリの Users ページからユーザーをフォロー解除するように設計されています。現在のユーザーで別のユーザーをフォロー解除すると、そのフォロワー数が減るのを確認できます。 ただし、ここではフィードページからユーザーをフォロー解除した後に現在のユーザーの following 数が減るのを確認しようとしています。 これを行うには、どのユーザー(現在のユーザーがフォロー解除されたユーザー)のカウンターを更新しようとしているかを知る必要があります。

3. ファイル routes.rb を開き、カウンターを更新するユーザーの ID を渡せる追加パラメーターを含むルートを追加します。

# routes.rb

resources :relationships, only: [:create, :destroy]
delete '/relationships/:id/:user_to_update', to: 'relationships#destroy_with_counter_update

パラメーターを渡してカスタムルートを使用するようになったため、unfollow リンクに使用するパスを更新する必要があります。



<%= link_to "unfollow", "/relationships/#{current_user.active_relationships.find_by(followed: micropost.user).id}/#{current_user.id}/",
                data: {turbo_method: :delete, turbo_confirm: 'Are you sure?'} %>

relationships_controller.rb を更新する際にパラメーターからユーザーを取得できるようになりました。

4. 以下のコードを relationships_controller.rb に追加します。

# relationships_controller.rb

def destroy_with_counter_update
  relationship = Relationship.find(params[:id])
  @unfollowed_user = relationship.followed
  current_user.unfollow(@unfollowed_user)

  @user = User.find(params[:user_to_update])
  @microposts = Micropost.where(:user_id => @unfollowed_user)

  respond_to do |format|
    format.html { redirect_to @unfollowed_user, status: :see_other }
    format.turbo_stream
  end
end

このメソッドはフィードからフォロー解除するユーザーを処理した後、即座に following カウンターを更新します。

この時点で format.turbo_stream の正体とそのメソッドの動作が気になっているかと思います。

Turbo は特殊な turbo_stream 形式をリクエストの Accept ヘッダーに挿入します(これは、フォームが POST、PUT、PATCH、または DELETE メソッドで送信される場合はデフォルトで行われますが、この形式を手動で GET リクエストに追加することも可能です。ドキュメントをご覧ください)。 その後、コントローラーはこのフォーマットで応答を送信するか、無視して HTML にフォールバックできます。 これがまさに RelationshipsController#destroy のコードで起きていることです。コントローラーは HTML 形式と Turbo Stream 形式の両方で応答を送信できます。

format.turbo_stream はコントローラー内でインラインで応答をレンダリングできます。

format.turbo_stream { render turbo_stream: turbo_stream.update "following", @user.following.count }

複数のターゲットを更新する場合は、.turbo_stream.erb 拡張を使って対応するビューテンプレートを使用するのが最適です。

5. relationships/destroy_with_counter_update.turbo_stream.erb という名前のファイルを作成し、そこに以下のコードを貼り付けます。

<%= turbo_stream.update "followers" do %>
  <%= @user.followers.count %>
<% end %>
<%= turbo_stream.update "following" do %>
  <%= @user.following.count %>
<% end %>
<%= microposts = Micropost.where(:user_id => @unfollowed_user) %>
<% microposts.each do |post| %>
  <%= turbo_stream.remove post %>
<% end %>

Following 数を更新するコードを詳しく見てみましょう。

<%= turbo_stream.update "following" do %>
  <%= @user.following.count %>
<% end %>

これらの行は何を行うのでしょうか?

コントローラーはリクエストへの応答で HTML を送信する必要があります。 Turbo ではページ全体を送信せずに HTML の一部を送信できます。 この場合は全ページが再読み込みされず、その部分のみが更新されます。 これらの行ではコントローラーが送信する Turbo Stream メッセージを指定しています。

そのメッセージの HTML は以下のフォームを取ります。

  
    The contents of this tag will be applied to the target element as per the specified action, e.g. deleted, appended, replaced, etc.
  

Turbo Stream では、append、prepend、replace、update、remove、before、after、および morph の合計 8 個のアクションを実行できます。 詳細については、ドキュメントをご覧ください。

ではもう一度、destroy.turbo_stream.erb に追加したコードを見てみましょう。

<%= turbo_stream.update "following" do %>
  <%= @user.following.count %>
<% end %>

following ターゲットupdate アクション を実行し(この ID の DOM 要素は shared/_stats.html.erb の部分にあります)、ターゲットを @user.following.count HTML で更新したいと考えています。

では、フィードからフォロー解除されたユーザーによるすべての投稿を削除するコードを見てみましょう。

<%= microposts = Micropost.where(:user_id => @unfollowed_user) %>
<% microposts.each do |post| %>
  <%= turbo_stream.remove post %>
<% end %>

Turbo Stream によるフォロー解除

ご覧のとおり、フィードから直接ユーザーをフォロー解除できるようになりました。 following 数がリアルタイムで正しく更新され、投稿がフィードから消えます。これらすべてをページ全体を再読み込みせずに行えます!

ブロードキャストの使用方法

同じ目的をブロードキャストでも達成できます。

Turbo Stream はレコードへの更新時にモデルからブロードキャストできます。 ブロードキャストできる基本的なアクショにンは、remove、replace、append、repend、before、after、および update があります。 詳細については、ドキュメントをご覧ください。

ここでは 1 人のユーザーが別のユーザーをフォロー解除した場合、つまり Relationship のインスタンスが破棄された場合に更新をブロードキャストします。

まず、RelationshipsController#destroy のレコードを破棄する必要があります。

  1. relationships_controller.rb を開き、destroy_with_counter_update メソッドまで移動したら、以下の行を追加します。
#relationships_controller.rb

relationship.destroy!

ブロードキャストはモデルのコールバックで行われるため、先にレコードを明示的に破棄する必要があります。

2. ファイル relationship.rb を開き、以下の行を追加します。

# relationship.rb

after_destroy { broadcast_update_to "following_stream_#{follower.id}", target: "following", html: "#{follower.following.count}" }

after_destroy は Relationship レコードが破棄された後に実行されるコールバックです。 broadcast_update_toupdate アクションを Turbo Stream にブロードキャストできるメソッドです。 他のアクションもブロードキャストできます。

ブロードキャストの補完

broadcast_update_later_to への引数を見てみましょう。 まず、ブロードキャスト先のストリームを指定する必要があります。 ここでは適切な情報を適切なユーザーに確実にブロードキャストできるよう、ユーザー ID に応じたストリーム名を選びます。そうでない場合、現在のユーザーだけでなく、全ユーザーのカウンターが少なくなります。 したがって、ストリームを following_stream_#{follower.id} と名付けましょう。 また、ターゲットを指定する必要もあります。この時点でも ID following と同じ DOM 要素になっています。 最後に、レンダリングする HTML を提供する必要があります。 多くの場合は部分ビューであり、この場合の引数は以下のようになります。

after_destroy { broadcast_update_later_to "[stream name]", target: "[target ID]", partial: "[partial name]", locals: { ... } }

ここで HTML を渡すにはこれで十分です。

ブロードキャストを受け入れるため、ビューファイルで対応する名前のストリームを購読する必要があります。

3. ファイル shared/_stats.html.erb を開きます。 先頭行の後に以下を追加します。



<% @user ||= current_user %>

<%= turbo_stream_from "following_stream_#{@user.id}" %>

destroy.turbo_stream.erb に追加した following カウンターを更新するコードも忘れずに削除しましょう。これは、代わりにブロードキャストで更新されるようになったためです。

これでページを読み込んでユーザーをフォロー解除すると、リアルタイムでカウンターが更新されるのを確認できます。

コントローラーから Turbo Stream を送信する代わりに、いつブロードキャストを使用すればよいですか?

以下の場合にはブロードキャストを使用することをお勧めします。

  • 更新を現在のユーザーだけでなく、同じページを表示している全員に表示する必要がある。
  • 更新が非同期である。

どちらも必要なければ、コントローラーから Trubo Stream を使用できます。

まとめ

このチュートリアルでは Turbo フレームワークとその Turbo Stream と Turbo Frame という基本的な概念について説明しました。 Turbo Frame と Turbo Stream を Rails アプリケーションで使用し、ページの一部をリアルタイムに更新できることを学びました。

Turbo Frame では動的かつ部分的な更新を特定の DOM 要素に適用し、Turbo Stream では複数の要素をターゲットにしてさまざまなアクションを実行できます。 ただし、Turbo Stream の能力を超える対話性を持たせたい場合は JavaScript を使用しなければなりません。

幸い、Hotwire はまさにその目的に合致した Stimulus という別のコンポーネントを提供しています。

Stimulus の詳細については、次のブログ記事をお待ちください。

Ruby および Rails 用の JetBrains IDE で Hotwire のサポートを活用しましょう。 最新の RubyMine バージョンは弊社ウェブサイトか無料の Toolbox App からダウンロードできます。

最新機能のリリース時にその内容を詳しく知るには、RubyMine のX をフォローしてください。

以下のコメント欄で皆さんのご感想をお聞かせください。また、新機能の提案と投票は課題トラッカーをご利用ください。

Happy Developing!

RubyMine チーム一同より

オリジナル(英語)ブログ投稿記事の作者:

image description