TurboFrameSrc
TurboFrameSrc gives your Rails app back a piece of information Turbo doesn't expose natively: the original src of the turbo-frame that triggered the current request.
The problem
Imagine a common Hotwire pattern: a lazy-loaded turbo-frame renders a filterable, paginated collection.
<%= turbo_frame_tag "orders", src: orders_path %>
Filtering is implemented once, generically, through a shared concern (e.g. a Filterable concern included in several controllers). That concern also exposes a generic reset_filters action so users can clear the filters that apply to whatever controller/action they're currently on — without wiping out unrelated filters living elsewhere on the page.
To reset the filters "in place" and re-render the same frame, that generic action needs to know which URL originally loaded the frame (orders_path, in the example above) so it can redirect or respond with the right content. Turbo does not give you this for free:
- The current request's URL is the action's URL (e.g.
/orders/reset_filters), not the frame's originalsrc. - The
Turbo-Framerequest header only tells you which frame is requesting the update, not what URL it was first loaded from.
Once your filters are reset, you're stuck without knowing where to send the user back to.
The solution
TurboFrameSrc tracks each turbo-frame's initial src on the client, forwards it as a request header on every subsequent Turbo Frame fetch, and echoes it back so your Rails controllers/views can read it at any point in the request cycle — including from a generic, shared action like reset_filters.
class FiltersController < ApplicationController
include Filterable
def reset_filters
reset_filters_for(params[:scope])
redirect_to turbo_frame_src
end
end
<%= turbo_frame_tag "orders", src: turbo_frame_src || orders_path %>
How it works
- JavaScript listens for Turbo's
turbo:before-fetch-requestevent. The first time a giventurbo-framefires a request, its currentsrc(or the page URL, if it has none yet) is stashed in adata-initial-srcattribute on the frame element, then sent as theX-Turbo-Frame-Srcrequest header on this and every following fetch initiated by that frame. - Rack middleware (
TurboFrameSrc::Middleware) reads the incomingX-Turbo-Frame-Srcheader (only for requests that also carry Turbo's ownTurbo-Frameheader) and re-attaches it to the response headers, so it survives redirects and stays available across the request/response cycle. - A view/controller helper,
turbo_frame_src, reads that header back for you:
module TurboFrameSrc::Helper
def turbo_frame_src
request.headers['X-Turbo-Frame-Src']
end
end
Because the whole flow is generic (frame name agnostic), it works the same way no matter which lazy-loaded frame, controller, or action is involved — which is exactly what a shared/generic reset_filters action needs.
Installation
Add the gem to your Gemfile:
bundle add turbo_frame_src
Ruby / Rails side
The gem ships a Rails engine. Simply requiring it (via Bundler) is enough to:
- register
TurboFrameSrc::Middlewarein your middleware stack, - include
TurboFrameSrc::HelperinActionController::Base(available in controllers and views), - register the JS package path with Importmap, if your app uses it.
JavaScript side
The client-side behavior is published to npm as turbo_frame_src.
Importmap
If your app uses importmap-rails, the engine already added the gem's JS path to the importmap. Just pin it:
bin/importmap pin turbo_frame_src
Then register it in your entrypoint:
import "turbo_frame_src"
Yarn / npm / Vite
yarn add turbo_frame_src
# or
npm install turbo_frame_src
import { registerTurboFrameSrc } from "turbo_frame_src"
registerTurboFrameSrc()
(Importing the package directly, without calling registerTurboFrameSrc, also works — the listener self-registers as soon as the module is loaded in a browser context.)
Usage
Once installed, use the turbo_frame_src helper anywhere you have access to the current request — controllers, views, or a shared concern/action — to retrieve the src the enclosing turbo-frame was originally loaded from:
turbo_frame_src # => "/orders?status=pending"
Typical use case: a generic reset_filters action, shared across several controllers via a concern, that needs to redirect back to whichever frame (or page) initiated the request:
module Filterable
extend ActiveSupport::Concern
def reset_filters
session.delete(filters_session_key)
redirect_to turbo_frame_src || request.referer
end
end
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/AlexisCro/turbo_frame_src. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the TurboFrameSrc project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.