Tailwind, Hotwire, more
Adam Wathan of TailwindCSS fame is doing a tremendous job rewriting CSS history and David Heinemeier Hansson, Mr Rails, just recently shared Hotwire. That begged a test!
By test I mean building a resource like say /accounts
to utilise both TailwindCSS goodness and Hotwire hotness.
Hotwir-ing an index looks something like this:
/ index.html.haml
= turbo_stream_from "accounts"
.table.min-w-full.divide-y.divide-gray-200
.table-row-group.table-fixed.bg-gray-50
.table-row
.table-cell.px-6.py-3.text-left.text-xs.font-medium.text-gray-500.uppercase.tracking-wider{:scope => "col"}
Account
.table-cell.px-6.py-3.text-left.text-xs.font-medium.text-gray-500.uppercase.tracking-wider{:scope => "col"}
Participant
.table-cell.px-6.py-3.text-left.text-xs.font-medium.text-gray-500.uppercase.tracking-wider{:scope => "col"}
Service plan
.table-cell.px-6.py-3.text-left.text-xs.font-medium.text-gray-500.uppercase.tracking-wider{:scope => "col"}
N/A
.table-cell.relative.px-6.py-3{:scope => "col"}
%span.sr-only Edit
.table-row-group{"x-max" => "2"}
= turbo_frame_tag "accounts" do
= render partial: "account", collection: @accounts
/ _account.html.haml
= turbo_frame_tag dom_id(account) do
.table-row.bg-white{"x-description" => "Odd row", id: dom_id(account) }
.table-cell.px-6.py-4.whitespace-nowrap.text-sm.font-medium.text-gray-900
= link_to account.name, edit_account_path(account)
.table-cell.px-6.py-4.whitespace-nowrap.text-sm.text-gray-500
/= #link_to account.participant.name,
.table-cell.px-6.py-4.whitespace-nowrap.text-sm.text-gray-500
= account.service_plan
.table-cell.px-6.py-4.whitespace-nowrap.text-sm.text-gray-500
\-
.table-cell.px-6.py-4.whitespace-nowrap.text-right.text-sm.font-medium
/ %a.text-indigo-600.hover:text-indigo-900{:href => "#"} Edit
= link_to 'Destroy', account, method: :delete, data: { confirm: 'Are you sure?' }
= turbo_stream_from "accounts"
tells Rails to add a pint of HTML for the client to ponder on (or really the Javascript resource which Rails throws into the mix) and start 'subscribing' to a stream which Rails sets up, and keeps publishing to once someone/something Create | Update | Delete accounts.
= turbo_frame_tag "accounts" do
instructs Rails to wrapping the entire set of 'details' in a custom tag <turbo-frame id="accounts"></turbo-frame>
= turbo_frame_tag dom_id(account) do
on every detail line does even so – wrapping every row in another <turbo-frame id="accounts"></turbo-frame>
This, however, kills any correct table cell width calculations (probably because the turbo_frame_tag
gets in the way of the cells?)
Here's a hack! [ Disclaimer: I'm sure there is a perfectly natural explanation and that I'm just not RTFM carefully ] Meanwhile:
/ index.html.haml
.table-row-group{"x-max" => "2", id: "accounts"}
/ = turbo_frame_tag "accounts" do
= render partial: "account", collection: @accounts
/ _account.html.haml
/ = turbo_frame_tag dom_id(account) do
.table-row.bg-white{"x-description" => "Odd row", id: dom_id(account) }
Yep - I'm waiting in anticipation for someone with all the 'brass' to come ridiculing me of forgetting something but I've not been able to google the correct way to get this to work!
Now my Hotwire will realtime update my table – and render my index with auto-calculated table cells; and I'm happy!
If you'd like to have your cake and eat it – like reaping all the fruits from this "hotwiring" thing, you'll enhance your controller actions somewhat too:
def create
@account = Account.new(account_params)
respond_to do |format|
if @account.save
format.turbo_stream { head 200 }
format.html { redirect_to accounts_url, notice: "Account was successfully created." }
format.json { render :show, status: :created, location: @account }
else
format.turbo_stream { render turbo_stream: turbo_stream.replace( @account, partial: "accounts/form", locals: { account: @account } ) }
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @account.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /accounts/1 or /accounts/1.json
def update
respond_to do |format|
if @account.update(account_params)
format.turbo_stream { render turbo_stream: turbo_stream.replace( @account ) }
format.html { redirect_to accounts_url, notice: "Account was successfully updated." }
format.json { render :show, status: :ok, location: @account }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @account.errors, status: :unprocessable_entity }
end
end
end
# DELETE /accounts/1 or /accounts/1.json
def destroy
@account.destroy
respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.remove( @account ) }
format.html { redirect_to accounts_url, notice: "Account was successfully destroyed." }
format.json { head :no_content }
end
end
The important parts being to add format.turbo_stream
to each action in order to not reloading the entire 'page'.
Job done.