This article was originally published on avo.cool
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-frame
s 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 ๐ช