Show spinner everytime async frame reloads

Hi everyone,

I was trying to figure out how to show a spinner inside my turbo-frame on every turbo request.

The first time the page loads, the spinner shows, and the turbo-frame content gets replaced
asynchronously with the content loaded from “src”.

So far so good.

The problem is when I initiate a subsequent turbo navigation within the frame, the loader that was rendered on the initial page load doesn’t show anymore.

From the user’s perspective, the form submits and 10 seconds later the turbo-frame’s content gets switched out with the new HTML (this is a very slow endpoint).

So I wanted to re-display the loader that I had on the initial page load on every turbo request.

This is how I achieved it, but I would like to know if there is a better way?

<!-- This is in the initial document -->
<turbo-frame id="my_frame" src="/my/endpoint" data-controller="turbo-frame-async-fix">
  <div class='my-loader'></div>
</turbo-frame>

When a turbo navigation occurs inside the frame, this stimulus controller will detect
it by observing the busy attribute of the turbo-frame, and replace it’s children with
the initial content

// controllers/turbo_frame_async_fix_controller.js

import { Controller } from '@hotwired/stimulus'
import { useMutation } from 'stimulus-use'

export default class extends Controller {
  connect() {
    // Copy the initial content of the turbo-frame, so we can put it back in when frame becomes busy.
    this.loaderHTML = [...this.element.childNodes].map((node) => node.cloneNode(true))

    useMutation(this, { attributes: true })
  }

  mutate(entries) {
    entries.forEach((mutation) => {
      if (mutation.type === 'attributes' && mutation.attributeName === 'busy') {
        if (mutation.target.attributes.busy) {
          this.element.replaceChildren(...this.loaderHTML)
        }
      }
    })
  }
}
1 Like

You could just use pure css for this using turbo-frame[busy]:

Just place the loader beneath the turbo-frame and position it over the frame. Try messing with this JSFiddle by manually removing/adding the busy flag from the <turbo-frame> tag.
https://jsfiddle.net/fhbu7s3v/10/

5 Likes

I ended up adapting @tleish’s solution slightly. To avoid having to add the .loading-ring div beside all of your turbo-frame tags, you can use CSS pseudo classes for a slightly different approach

@keyframes spinner {
  to { transform: rotate(360deg); }
}

[busy]:not([no-spinner]) {
  position: relative;
}

[busy]:not([no-spinner]) > * {
  opacity: 0.25;
}

[busy]:not([no-spinner])::after {
  content: '';
  box-sizing: border-box;
  position: absolute;
  top: 12rem;
  left: 50%;
  width: 2.5rem;
  height: 2.5rem;
  margin-top: -1rem;
  margin-left: -1rem;
  border-radius: 50%;
  border: 0.275rem solid rgba(237, 233, 254, 0.7); /* Semi-transparent border */
  border-top-color: rgb(124, 58, 237); /* Solid color for the top border */
  animation: spinner 0.6s linear infinite;
  z-index: 10;
}

The above includes an opt-out by adding no-spinner to your turbo frame tags. For example, <turbo-frame no-spinner>

1 Like