hotrails.dev

Chapter 2
Organizing CSS files in Ruby on Railsでcssを追加して、適用するためにサーバー再起動した際に
Expected to find a manifest file in app/assets/config/manifest.js
というエラーが発生した。
Sprocketsを使用してアセットパイプラインを管理しようとしていて発生した問題らしいが、Rails 7 では、デフォルトでSprocketsの代わりにPropshaftがアセットパイプラインとして使用されるらしい。
Gemfileを見るとたしかにgem "sprockets-rails"
があったので、こちらを消してgem 'propshaft'
としてbundle install
し直すことで解決した。
sprockets-railsを追加した覚えはないので、不可解だが、これで解決した

rails7ではデフォルトでTurbo Driveは有効になっている。
これによりリンクとかフォームの挙動が全てインターセプトされてajax通信に書き換えられる。
またajaxのレスポンスを受けて<body>タグを更新するが、<head>タグは更新されない。
これによりリクエストの度にフォント、CSS、JSのファイルのダウンロードされることはなくなるので速くなるという理屈らしい。
そこらへんのDLってそんなに時間かかるんだという感想を持った。
確かにファイルのDLが無いに越したことはないとは思うが。
このajaxに変更する動きの影響により、フォーム送信でエラーが発生した際は、 422 status code :unprocessable_entity
を返さないといけないらしい。
scaffoldでコントローラー作った際は、createとupdateに自動でstatus: :unprocessable_entity
を付与してくれるらしい。
Turbo Driveを一部で動かしたくない時はdata-turbo="false"
で簡単に停止できる。
<main class="container">
<div class="header">
<h1>Quotes</h1>
<%= link_to "New quote",
new_quote_path,
class: "btn btn--primary",
data: { turbo: false } %>
</div>
<%= render @quotes %>
</main>
アプリ全体でTurbo Driveを停止したい場合はapp/javascript/application.js
に以下を追記する
import { Turbo } from "@hotwired/turbo-rails"
Turbo.session.drive = false
<head>タグが更新されないと、CSSファイルが更新されても反映されない可能性がある。
Turbo Driveはリクエストごとに<head>タグのDOMを比較して、変更があればpage全体を更新する処理をしてくれる。
これは"data-turbo-track": "reload"
の設定で行われるし、デフォルトでは<%# app/views/layouts/application.html.erb %>
に以下の設定があるのでアプリ全体に適用される
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>

Chapter 4 Turbo Frames and Turbo Stream templates
Turbo Frames are independent pieces of a web page that can be appended, prepended, replaced, or removed without a complete page refresh and writing a single line of JavaScript!
「Turbo Frames は、ページ全体をリフレッシュすることなく、JavaScript のコードを一行も書かずに、ウェブページの一部を追加、前置、置換、または削除できる独立した要素です。」
<turbo-frame id="first_turbo_frame">
で囲むとその中のフォーム送信やリンクのクリックをインターセプトして、frame内を独立したwebページとして扱えるようになる
Rule1: turbo frame内のリンクをクリックすると、turboは対象ページに同じIDのframeがあることを期待する。
なのでindexのturbo frame内でnewのリンクをクリックしたら、newのフォームにもindexと同じIDと割り振る必要がある。
index.html.erb
<%= turbo_frame_tag "first_turbo_frame" do %>
<div class ="header">
<h1>Quotes</h1>
<%= link_to "New quote", new_quote_path, class: "btn btn-primary" %>
</div>
<% end %>
new.html.erb
<%= turbo_frame_tag "first_turbo_frame" do %>
<%= render "form", quote: @quote %>
<% end %>
Rule2: turbo frame内のリンクをクリックした時、同じturbo frame IDを持つページがない場合は、frameが消えてエラーレスポンスがコーンソールログに表示される。
RUle3: data-turbo-frame attriburteを使用することで。リンクは1つ以上のframeに紐づけられることができる
_top
という特別なframeを使用するとページ全体を表すことができる。つまりページ全体を置換することができる。
全てのページはデフォルトでこの_top
を持っている。
この_top
を使うとページのURLも書き換わる
dom_id
という便利なヘルパーメソッドがあるので
<%= turbo_frame_tag @quote do %>
と書くだけで、各@quoteのIDを埋め込んだturbo frameが生成される。
turbo_frame_tag
内のリンクが機能しない際は"_top"を使うことでページ全体を置換する
<%= turbo_frame_tag quote do %>
<div class="quote">
<%= link_to quote.name, quote_path(quote), data:{turbo_frame: "_top"} %>
同様にturbo_frame内のdeleteメソッドが機能しない際は、button_to
にdata:{turbo_frame: "_top"}
を追加することで、ページ全体が置き換わり、削除ができる
<%= 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, data:{turbo_frame: "_top"},class: "btn btn--light" %>
<%= link_to "Edit", edit_quote_path(quote), class: "btn btn-light" %>
</div>
</div>
<% end %>
現状だと、quote2をedit状態にした状態でquote3を削除すると、quote2のedit状態が解除される。
これをdelete対象のもの以外の状態を維持するために、turbo stream formatが使用できる。
respond_to
内でformat.turbo_stream
をすることで、TURBO_STREAM formatsを使用することができる。
def destroy
@quote.destroy
respond_to do |format|
format.html { redirect_to quotes_path, notice: "Quote was successfully destroyed." }
format.turbo_stream
end
end
そうるすと対応するerbが呼ばれる。
<%# app/views/quotes/destroy.turbo_stream.erb %>
<%= turbo_stream.remove @quote %>
そのなかで指定されたturbo_streamのアクション付きのHTMLが返却される。
<turbo-stream action="remove" target="quote_908005780">
</turbo-stream>
ブラウザがこのHTMLを受取り、指定されたアクションを実行する。
turbo streamには他にも以下のアクションがある
# Remove a Turbo Frame
turbo_stream.remove
# Insert a Turbo Frame at the beginning/end of a list
turbo_stream.append
turbo_stream.prepend
# Insert a Turbo Frame before/after another Turbo Frame
turbo_stream.before
turbo_stream.after
# Replace or update the content of a Turbo Frame
turbo_stream.update
index内で新規作成フォームを作る方法。
- indexファイル内に空のフォームを作成しておく
- indexファイル内に新規作成へのリンクを置く。そのリンクに新規作成のturbo_frameを設定する。
- newファイル内で新規作成のturbo_frameを設定する。
これをすることで新規作成された要素が、1.の空のフォームに配置される
<%# app/views/quotes/index.html.erb %>
<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) } %> # このリンクが押された時にnew_quoteというturbo_frameを持つファイルが期待される
</div>
<%= turbo_frame_tag Quote.new %> # ここにnew_quoteというturbo_frameを持つファイルが期待される
<%= render @quotes %>
</main>
<%# app/views/quotes/new.html.erb %>
<main class="container">
<%= link_to sanitize("← Back to quotes"), quotes_path %>
<div class="header">
<h1>New quote</h1>
</div>
<%= turbo_frame_tag @quote do %> # @quoteが新規のインスタンスなのでnew_quoteというturbo_frameが生成され、期待される箇所に割り当てられる
<%= render "form", quote: @quote %>
<% end %>
</main>
新規作成したquoteをindexの先頭に表示する方法
- controller内でturbo_steamで返すように設定する
- turbo_stream.erbの設定で、新規作成した要素をprependする設定と、新規作成フォームを初期化する設定をする
- indexファイル内で@quotesをturbo_frame_tagで囲む
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 %>
<%= turbo_stream.prepend "quotes", partial: "quotes/quote", locals: { quote: @quote } %>
<%= turbo_stream.update Quote.new, "" %>
<%# app/views/quotes/index.html.erb %>
...
<%= turbo_frame_tag "quotes" do %>
<%= render @quotes %>
<% end %>

chapter5 Real-time updates with Turbo Streams
Turbo StreamはAction Cableと組み合わせることで、リアルタイム通信を実現できる。(チャットなど)
Quotes#index
に表示された一覧にリアルタイム通信でquoteを追加削除する
class Quote < ApplicationRecord
# All the previous code
after_create_commit -> { broadcast_prepend_to 'quotes', partial: 'quotes/quote', locals:{quote: self}, target: 'quotes'}
end
Quoteモデルに上のcallbackを設定する。
after_create_commit
はDBにレコードに新規登録されるたびに実行される。
broadcast_prepend_to "quotes", partial: "quotes/quote", locals: { quote: self }, target: "quotes"
の部分は、作成されたquoteのHTMLが、quotes
をサブスクライブしているuserにブロードキャストされ、quotes
のDOMリストにprepend(先頭に追加)する。
※DOMリストの最後に追加したい場合はbroadcast_append_to
を使う
broadcastを受け取りたい箇所で以下の様にして通信を受け取るhelperを追加する
<%= turbo_stream_from "quotes" %>
これは以下のようなHTMLを作成する。
<turbo-cable-stream-source
channel="Turbo::StreamsChannel"
signed-stream-name="very-long-string"
>
</turbo-cable-stream-source>
これによりsigned-stream-name
の名前のchannelをサブスクライブする。
signed-stream-name
は、turbo_stream_from
で引数として渡したquptes
を署名化して、悪意ある第3者のユーザーが改ざんできないようにし、また受け取るべきでないHTMLを受け取れないようにする。
この時redisが必要になるため、設定が必要
config/cable.yml
で
development:
adapter: redis
url: redis://localhost:6379/1
と追加が必要。
自分の場合はdockerで行っているため
compose.ymlを以下の様に更新した。
services:
redis:
image: redis:alpine
restart: always
db:
image: postgres
# dbのユーザー名とパスワードでこれが無いとdbが起動できなかった
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
# 無くても動くけど指定しておくとdocker-composeを停止してもdbの内容が永続化されるため、指定することが多いと思われる
# https://matsuand.github.io/docs.docker.jp.onthefly/storage/volumes/
volumes:
- postgres_volume:/var/lib/postgresql/data
# 無くても動くが指定しておくとコンテナ停止時にサービスが再起動してくれる
# https://docs.docker.jp/v19.03/config/container/start-containers-automatically.html
restart: always
web:
build: .
# tmp/pids/server.pidが残ってたら `A server is already running. ~~` のエラーでrailsを起動できないので事前に消してから、`rails sever` する
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
# 上記のdbイメージで指定したユーザー名とパスワードをrails側でも指定するため環境変数に設定。
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
TZ: "Asia/Tokyo"
# ホストのカレントディレクトリ(.)とイメージ内の/myappディレクトリを同期させている
# volumes:
# - .:/myapp
volumes:
- type: bind
source: ./
target: /myapp
- type: volume
source: gemdata
target: /usr/local/bundle
- type: volume
source: node_modules
target: /myapp/node_modules
ports:
- "3000:3000"
restart: always # コンテナが停止すると常に再起動
tty: true # 疑似ターミナル (pseudo-TTY) を割り当て。https://docs.docker.jp/compose/compose-file/index.html#tty
stdin_open: true # サービス コンテナに標準入力を割り当てて実行するよう設定(https://docs.docker.jp/compose/compose-file/index.html#stdin-open)。
depends_on:
- db
- redis
volumes:
postgres_volume:
gemdata:
node_modules:
またこのredisのconainerを使うため
config/cable.yml
は以下とした
development:
adapter: redis
url: redis://redis:6379
Error loading the 'redis' Action Cable pubsub adapter. Missing a gem it depends on? redis is not part of the bundle. Add it to your Gemfile.
のエラーが発生した。
gemfileの以下のコメントアウトを外し、bundleしてgemを追加した。
# Use Redis adapter to run Action Cable in production
gem "redis", "~> 4.0"
その後bin/rails turbo:install
コマンドを実行した。
結果は以下で、
root@41742cb7a2a4:/myapp# bin/rails turbo:install
Import Turbo
unchanged app/javascript/application.js
Install Turbo
run yarn add @hotwired/turbo-rails from "."
Run turbo:install:redis to switch on Redis and use it in development for turbo streams
root@41742cb7a2a4:/myapp#
Run turbo:install:redis to switch on Redis and use it in development for turbo streams
ということは、こちらのコマンドを実行する必要があるのかと思ったが、この時点で問題なく動いたので関係なかった。
実はprodctionモードで動いてるのかと思ったが、"RAILS_ENV"=>"development",
だったのでそんなこともなかった
railsコンソールでQuoteを新規作成した結果
=> #<Quote:0x0000ffffb6fc9228 id: 908005772, name: "broadcast test", created_at: Sat, 10 Aug 2024 04:50:55.028340000 UTC +00:00, updated_at: Sat, 10 Aug 2024 04:50:55.028340000 UTC +00:00>
irb(main):002> Quote.create!(name: "broadcast test2")
TRANSACTION (0.4ms) BEGIN
Quote Create (0.7ms) INSERT INTO "quotes" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "broadcast test2"], ["created_at", "2024-08-10 04:51:04.393186"], ["updated_at", "2024-08-10 04:51:04.393186"]]
TRANSACTION (1.3ms) COMMIT
Rendered quotes/_quote.html.erb (Duration: 0.6ms | Allocations: 285)
[ActionCable] Broadcasting to quotes: "<turbo-stream action=\"prepend\" target=\"quotes\"><template><turbo-frame id=\"quote_908005773\">\n <div class=\"quote\">\n <a data-turbo-frame=\"_top\" href=\"/quotes/908005773\">broadcast test2</a>\n <div class=\"quote__actions\">\n <form class=\"button_to\" method=\"post\" action=...
=> #<Quote:0x0000ffffb6501b88 id: 908005773, name: "broadcast test2", created_at: Sat, 10 Aug 2024 04:51:04.393186000 UTC +00:00, updated_at: Sat, 10 Aug 2024 04:51:04.393186000 UTC +00:00>
irb(main):003> ENV
insertと同時にBroadcastingされ、画面にも反映された
targetの部分はデフォルトではmodel_name.plural
となるので省略できる。
before
after_create_commit -> { broadcast_prepend_to "quotes", partial: "quotes/quote", locals: { quote: self }, target: "quotes" }
after
after_create_commit -> { broadcast_prepend_to 'quotes', partial: 'quotes/quote', locals:{quote: self}}
partial
とlocals
についてもデフォルト値があり、省略できる。
partial
のデフォルト値はto_partial_path
と一緒なので、Quote
モデルのデフォルト値は"quotes/quote"となる
locals
のデフォルト値は{ model_name.element.to_sym => self }
であり、Quote
モデルの場合、{ quote: self }となる
そのため最終的なリファクタ後の形式は
after_create_commit -> { broadcast_prepend_to "quotes" }
となる。
create,update,destroyそれぞれの時にリアルタイム通信するためのコードが以下となる
after_create_commit -> { broadcast_prepend_to 'quotes'}
after_update_commit -> { broadcast_replace_to 'quotes'}
after_destroy_commit -> { broadcast_remove_to 'quotes'}
またこのリアルタイム通信を非同期にするために以下の様に変更される。
ただし削除については非同期にできない
after_create_commit -> { broadcast_prepend_later_to "quotes" }
after_update_commit -> { broadcast_replace_later_to "quotes" }
after_destroy_commit -> { broadcast_remove_to "quotes" }
このような3つの設定を1つにまとめられるのが以下のコード
broadcasts_to -> (qupte){'quotes'}, inserts_by: :prepend

PCをスリープしてから再度開いたら謎にwidth
が上昇し続ける現象に遭遇

chapter6 Turbo Streams and security
この章ではセキュリティについて学ぶ。
重要情報が、第3者にbradcastされると困るため
色々と設定して、
companyテーブルとuserテーブルを作成
quoteとuserはcompanyに所属する。
deviseによるログイン機能を追加
quoteは自分の会社のもののみ表示するよう改修する。
この状態で、2つのwindowでそれぞれ違う会社のユーザーでログインする。
会社1のユーザーでquoteを作成すると会社2のユーザーにも表示される。
これはturbo_stream_from
で作成されたturbo-cable-stream-source
のsigned-stream-name
が同じになっているため、送信を受け付けてしまうから。
app/models/quote.rb
broadcasts_to ->(quote) { "quotes" }, inserts_by: :prepend
このコードは、quoteモデルの変更が常に、quotes
というチャンネルにbroadcastされることを表す。
app/views/quotes/index.html.erb
<%= turbo_stream_from "quotes" %>
そしれこれはturbo_stream_from
でquotes
を指定してsubscribeしているため、ここで受信される。
これが原因であるため、会社ごとに違うチャンネル名を作成する必要がある。
app/models/quote.rb
broadcasts_to ->(quote) { [quote.company,"quotes"] }, inserts_by: :prepend
このように変更することにより、lambdaに渡されたarrayからsigned stream nameが生成されるため、会社ごとに違うsigned stream nameとなる。
また、これを受信するために
app/views/quotes/index.html.erb
<%= turbo_stream_from current_company ,"quotes" %>
と変更する必要がある
これにより、確かに会社ごとに違うsigned stream nameが生成され、別の会社のquote情報を受け取ることがなくなった。

#7 Flash messages with Hotwire
turboでflashメッセージを追加する方法とStimulusでアニメーションを作る方法
まずturboがない状態でflashを動作させる必要がある
app/javascript/application.js
import { Turbo } from "@hotwired/turbo-rails"
Turbo.session.drive = false
とあるが、実際はこれでは効かなかった。
原因は謎。
サーバ再起動やdockerの再buildを試したが効果はなかった。
turbo driveを無効化するのは今度調べるとして、今回はturbo driveがある状態でflashを表示する方法をまとめる。
Turbo Streamがある状態では、turbo_streamの戻り値にflash情報を詰める必要がある
respond_to do |format|
format.html { redirect_to quotes_path, notice: "Quote was successfully created." }
format.turbo_stream { flash.now[:notice] = "Quote was successfully created." }
end
またturbo_streamのviewファイルにもflashの情報を追加する必要がある。
app/views/quotes/create.turbo_stream.erb
<%= turbo_stream.prepend "flash", partial: "layouts/flash" %>
複数のflashを表示したい場合は、turbo_stream.prepend
画面に1つのflashしか表示しない場合は、turbo_stream.replace
またこの場合、flashというIDを持つDOMがまだないので、データの行き先が無い。
そのため
app/views/layouts/application.html.erb
に以下を追加する
<div id="flash" class="flash">
<%= render "layouts/flash" %>
</div>
また同様の処理をupdate、destroyなどにも追加する必要があるが、処理を共通化できる
app/helpers/application_helper.rb
def render_turbo_stream_flash_messages
turbo_stream.prepend "flash", partial: "layouts/flash"
end
app/views/quotes/create.turbo_stream.erb
、app/views/quotes/update.turbo_stream.erb
、app/views/quotes/destroy.turbo_stream.erb
<%= render_turbo_stream_flash_messages %>

#8 Two ways to handle empty states with Hotwire
この章では、Turboで空の状態をどのように扱うかを学ぶ。
1つ目は Turbo FramesとTurbo Streamsを使う方法
2つ目はonly-child CSS pseudo-classだけを使う方法
quoteが空の状態の時に、「メッセージがまだありません」というようなガイダンスが表示されたほうが親切
それを実現するために、まずqupteがない状態の時に表示するファイルを作成
app/views/quotes/_empty_state.html.erb
<div class="empty-state">
<p class="empty-state__text">
You don't have any quotes yet!
</p>
<%= link_to "Add quote", new_quote_path, class: "btn btn--primary" %>
</div>
app/views/quotes/index.html.erb
を改造して、quoteが無い場合にこのファイルをrenderするよう設定する。`
<%= turbo_stream_from current_company, "quotes" %>
<div 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 do %>
<% if @quotes.none? %>
<%= render "quotes/empty_state" %>
<% end %>
<% end %>
<%= turbo_frame_tag "quotes" do %>
<%= render @quotes %>
<% end %>
</div>
この状態だと、全quoteを削除した時には表示されるが、一度quoteを新規作成し、削除した後では再度表示されない。
destroy.turbo_stream.erb
内で、new_quote
のIDを持つTurbo Frameのコンテントをquotes/empty_state
で更新しなければいけない。
app/views/quotes/destroy.turbo_stream.erb
<%= turbo_stream.remove @quote %>
<%= render_turbo_stream_flash_messages %>
<% unless current_company.quotes.exists? %>
<%= turbo_stream.update Quote.new do %>
<%= render "quotes/empty_state" %>
<% end %>
<% end %>
こうすることで、再度quoteがなくなった際に、_empty_state.html.erb
の内容が表示される。
ただし、_empty_state.html.erb
が表示された状態で、別タブなどから新規quoteが作成されると、quoteがあるのに、_empty_state.html.erb
が表示されたままとなる。
この解決は次章で扱う。
またこれまでのことをonly-child CSS pseudo-classだけで達成可能。
まずapp/views/quotes/index.html.erb
内で、empty_stateファイルを、quotesリスト内でrenderするように変更
<%= turbo_stream_from current_company, "quotes" %>
<div 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/empty_state" %>
<%= render @quotes %>
<% end %>
</div>
app/assets/stylesheets/components/_empty_state.scss
で:only-child pseudo-class
を使い
quotes
というidを持つ唯一のTurbo Frameの場合にのみ表示するようにする。
app/assets/stylesheets/components/_empty_state.scss
.empty-state {
padding: var(--space-m);
border: var(--border);
border-style: dashed;
text-align: center;
&__text {
font-size: var(--font-size-l);
color: var(--color-text-header);
margin-bottom: var(--space-l);
font-weight: bold;
}
&--only-child {
display: none;
&:only-child {
display: revert;
}
}
}
またapp/views/quotes/_empty_state.html.erb
で、new_quote
というIDを明確に指定するよう変更する必要がある。(?)
<div class="empty-state empty-state--only-child">
<p class="empty-state__text">
You don't have any quotes yet!
</p>
<%= link_to "Add quote",
new_quote_path,
class: "btn btn--primary",
data: { turbo_frame: dom_id(Quote.new) } %>
</div>

#9 Another CRUD controller with Turbo Rails
quotesのdateを扱うCRUD Controllerの作成を通して、これまでの復習を行う
app/views/quotes/show.html.erb
内にline_item_date
の新規作成フォームが表示されるようにする
<main class="container">
<%= link_to sanitize("← Back to quotes"), quotes_path %>
<div class="header">
<h1>
<%= @quote.name %>
</h1>
<%= link_to "New date",
new_quote_line_item_date_path(@quote),
data: { turbo_frame: dom_id(LineItemDate.new) },
class: "btn btn--primary" %>
</div>
<%= turbo_frame_tag LineItemDate.new %>
<%= render @line_item_dates, quote: @quote %>
</main>
data: { turbo_frame: dom_id(LineItemDate.new) },
と<%= turbo_frame_tag LineItemDate.new %>
を追加
これによりQuotes#show page
ページにnew_line_item_date
というIDを持つturbo_fram_tag
を保持することになる
app/views/line_item_dates/new.html.erb
<main class="container">
<%= link_to sanitize("← Back to quote"), quote_path(@quote) %>
<div class="header">
<h1>New date</h1>
</div>
<%= turbo_frame_tag @line_item_date do %>
<%= render "form", quote: @quote, line_item_date: @line_item_date %>
<% end %>
</main>
<%= turbo_frame_tag @line_item_date do %>
の箇所を追加することで、new_line_item_dateというIDを持つ
turbo_fram_tagの内容を宣言する? この状態で、不正な情報でline_item_dateを作成するとエラー文言が画面に表示される。 これは
LineItemDates controllerが不正入力による
LineItemDates#newを生成するから これにより
new_line_item_date`のIDを持つTurbo Frame内にあるformが同じIDを持つ、エラー文言付きのviewで置換されている。
しかし、有効なフォームを送信すると、
フォームが new_line_item_date という ID を持つ Turbo フレーム内にあり、レスポンスが、空のフレームを持つ Quotes#show ページにリダイレクトされ、フォームが消えてる。
これは、フォームを含むTurboフレームが空のフレームに置き換えられるため。
これを回避するために、create.turbo_stream.erb テンプレートが必要になる。
create.turbo_stream.erb
内で以下をやる必要がある。
new_line_item_date
のIDのを持つturbo frameを空のformと置換する
正しい順番でになるようにline item dateリストに追加する
まずLineItemDatesController#create
をturbo_streamフォーマットに変更
app/controllers/line_item_dates_controller.rb
def create
@line_item_date = @quote.line_item_dates.build(line_item_date_params)
if @line_item_date.save
respond_to do |format|
format.html { redirect_to quote_path(@quote), notice: "Date was successfully created." }
format.turbo_stream { flash.now[:notice] = "Date was successfully created." }
end
else
render :new, status: :unprocessable_entity
end
end
正しい順番でになるようにline item dateリストに追加するために、
app/views/line_item_dates/create.turbo_stream.erb
を以下とする
<%# Step 1: remove the form from the Quotes#index page %>
<%= turbo_stream.update LineItemDate.new, "" %>
<%# Step 2: add the date at the right place %>
<% if previous_date = @quote.line_item_dates.ordered.where("date < ?", @line_item_date.date).last %>
<%= turbo_stream.after previous_date do %>
<%= render @line_item_date, quote: @quote %>
<% end %>
<% else %>
<%= turbo_stream.prepend "line_item_dates" do %>
<%= render @line_item_date, quote: @quote %>
<% end %>
<% end %>
<%= render_turbo_stream_flash_messages %>
Step 1ではformを持つturbo frameを空のformにする
Step 2では、新規作成したline_item_dateより古い日付のものがあれば取得して、新規作成したものを追加する。
もし古い日付のものがなければline_item_dateのリストにただ追加する。
これを機能させるためにline_item_dates
のIDを持つturbo_frame_tag
内の line item datesを囲む必要がある。
新規作成したline_item_date
を追加するために
app/views/quotes/show.html.erb
<%# All the previous code... %>
<%= turbo_frame_tag "line_item_dates" do %>
<%= render @line_item_dates, quote: @quote %>
<% end %>
<%# All the previous code... %>
またturbo_frame_tag内の各line item date
を囲む必要がある
これは各line item date をIDのによって特定できるようにするため。
特定のline item date
の後に挿入する必要があるときのため
pp/views/line_item_dates/_line_item_date.html.erb
<%= turbo_frame_tag line_item_date do %>
<div class="line-item-date">
<!-- All the previous code -->
</div>
<% end %>
編集機能の作成
最初に編集フォームがQuotes#show
pageのHTMLを置換する必要がある
app/views/line_item_dates/edit.html.erb
<main class="container">
<%= link_to sanitize("← Back to quote"), quote_path(@quote) %>
<div class="header">
<h1>Edit date</h1>
</div>
<%= turbo_frame_tag @line_item_date do %>
<%= render "form", quote: @quote, line_item_date: @line_item_date %>
<% end %>
</main>
この時点ですでにformによる置換ができるが、一方でline_item_dateの表示順を保証できない
まずはupdateメソッド内のTurbo Streamを更新する必要がある。
app/controllers/line_item_dates_controller.rb
def update
if @line_item_date.update(line_item_date_params)
respond_to do |format|
format.html { redirect_to quote_path(@quote), notice: "Date was successfully updated." }
format.turbo_stream { flash.now[:notice] = "Date was successfully updated." }
end
else
render :edit, status: :unprocessable_entity
end
end
app/views/line_item_dates/update.turbo_stream.erb
<%# Step 1: remove the form %>
<%= turbo_stream.remove @line_item_date %>
<%# Step 2: insert the updated date at the correct position %>
<% if previous_date = @line_item_date.previous_date %>
<%= turbo_stream.after previous_date do %>
<%= render @line_item_date, quote: @quote %>
<% end %>
<% else %>
<%= turbo_stream.prepend "line_item_dates" do %>
<%= render @line_item_date, quote: @quote %>
<% end %>
<% end %>
<%= render_turbo_stream_flash_messages %>
destroy action
app/controllers/line_item_dates_controller.rb
def destroy
@line_item_date.destroy
respond_to do |format|
format.html { redirect_to quote_path(@quote), notice: "Date was successfully destroyed." }
format.turbo_stream { flash.now[:notice] = "Date was successfully destroyed." }
end
end
app/views/line_item_dates/destroy.turbo_stream.erb
<%= turbo_stream.remove @line_item_date %>
<%= render_turbo_stream_flash_messages %>