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.

Well - not really like this though!

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!

This is how I'd like my table to look like!

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.