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 original src.
  • The Turbo-Frame request 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

  1. JavaScript listens for Turbo's turbo:before-fetch-request event. The first time a given turbo-frame fires a request, its current src (or the page URL, if it has none yet) is stashed in a data-initial-src attribute on the frame element, then sent as the X-Turbo-Frame-Src request header on this and every following fetch initiated by that frame.
  2. Rack middleware (TurboFrameSrc::Middleware) reads the incoming X-Turbo-Frame-Src header (only for requests that also carry Turbo's own Turbo-Frame header) and re-attaches it to the response headers, so it survives redirects and stays available across the request/response cycle.
  3. 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::Middleware in your middleware stack,
  • include TurboFrameSrc::Helper in ActionController::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.