Behind the scenes Dynamically re-use & lazy-load pages using Hotwire

Hotwire is a fantastic technology that helps you build dynamic websites without thinking about JavaScript.

When we re-wrote Avo from VueJS to Hotwire, when it first came out, we had to think about how we could leverage it to our advantage.

One of the first things we did was to add dynamic turbo-frames around common pages.

For example, the ResourceIndex page is the page that usually displays the table with the requested resources (it shows the users on /users). We knew that we could use that exact partial when we wanted to display the has_many association on the ResourceShow page but didn’t know how at first. We could have extracted it to a partial and rendered it in the ResourceShow page underneath the record details, but the ResourceShow page would take a long time to load when you have many associations to one record.

We then came up with the idea to use a turbo-frame for that but still didn’t want to use a partial.

Let’s say we have User and Team models like so:

class Team < ApplicationRecord
  has_many :users
end

class User < ApplicationRecord
  belongs_to :team
end

The routes and controllers look like this:

resources :posts

get "/:resource_name/:id/:related_name/", to: "associations#index"
class BaseController < ApplicationController
  def index
    # if there's a query set up, use it, if not, set one up
    unless defined? @query
      @query = @resource.class.query_scope
    end
  end
end

class AssociationsController < BaseController
  def index
    # find the parent record and set the query
    @query = @parent_model.public_send(params[:related_name])
  end
end

So what happens there? When a user goes to /users, they will see the list of users, and if they go to /teams/TEAM_ID/users, they should see the same list of users but scoped to the respective team.

In order to achieve this using turbo-frames we’ll add a lazy-loaded turbo frame on the RecordShow page with the src attribute set to the association path with the ?turbo_frame="has_many_users" param added to the path /teams/TEAM_ID/users?turbo_frame="has_many_users" url.

<!-- views/base/show.html.erb -->

<!-- record details here -->

<!-- association details below -->
<turbo-frame id="has_many_users" src="/teams/#{@team.id}/users?turbo_frame=has_many_users" target="_top">
  <!-- Loading state -->
</turbo-frame>

Next, we should add a dynamic wrap around the index.html.erb partial like so.

<!-- views/base/index.html.erb -->

<% if params[:turbo_frame].present? %>
  <turbo-frame id="<%= params[:turbo_frame] %>">
<% end %>

  <!-- The list of records -->

<% if params[:turbo_frame].present? %>
  </turbo-frame>
<% end %>

What happens is that when the user loads a teams Show page /teams/1, that page will display with the lazy-loaded frame (so no impact on the performance of that page on load), which in turn, loads the association Index page with the turbo_frame param. That will add the <turbo-frame id="has_many_users"> tag around the template allowing Turbo to replace the content dynamically on the page.

We’re re-using the actual index.html.erb template and the BaseController#index action.

Of course, this can be improved using some helpers and a partial.

# app/helpers/application_helper.rb

def turbo_frame_wrap(name, &block)
  render layout: "partials/turbo_frame_wrap", locals: {name: name} do
    capture(&block)
  end
end
<!-- app/views/partials/turbo_frame_wrap.html.erb -->

<% if name.present? %><turbo-frame id="<%= name %>"><% end %>
  <%= yield %>
<% if name.present? %></turbo-frame><% end %>
<!-- views/base/show.html.erb -->

<!-- record details here -->

<!-- association details below -->
<turbo-frame id="<%= turbo_frame %>" src="<%= frame_url %>" target="_top">
  <!-- Loading state -->
</turbo-frame>
<!-- views/base/index.html.erb -->

<%= turbo_frame_wrap(params[:turbo_frame]) do %>
  <!-- The list of records -->
<% end %>

You can do the same thing for Show pages too. We did with Avo.

You can find a more detailed example on Avo’s GitHub repo.

Stay cool and improve performance 💪

avo.cool

avo.cool/repo

avo.cool/feedback

avo.cool/try

© 2022 AvoHQ. All rights reserved.

Proudly made in Romania