Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Turbo Frameの読み込みプログレス表示をCSSだけで実現する(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

Rails: Turbo Frameの読み込みプログレス表示をCSSだけで実現する(翻訳)

RailsのページでTurbo Frameを使うと、初期読み込みでloading...のようなステート表示を設定できます。読み込みが完了すると、リクエストのbodyがTurbo Frameの要素に注入されて、このステート表示のテキストは置き換えられます。この機能は、アプリの一部を非同期に読み込むときに有用です。

しかしTurbo Frameの要素を、ページ上で恒久的に配置している場合はどうでしょうか?ページに表示するオーバーレイコンポーネントやモーダルダイアログで、時間のかかるものを表示すると、デフォルトではリソースが読み込まれるまで何も表示されません(500ms後にページ上部に表示される細いプログレスバーは除きます)。これではユーザーのエクスペリエンスが悪くなります。

「読み込み中」テキストを表示するときも、キビキビ表示する方が、現在の状態をユーザーに的確に伝えられるので理想的です。

以下のGIF動画をご覧ください。

上は初回の表示です。以下でフレームを「リロード」した場合と見比べてみましょう。

  • 1番目の"Reload second"をクリックすると、Loading...もスピナーも表示せずに、"First frame loaded..."が即座に"First frame reloaded..."に変わります。
  • 2番目の"Load second"をクリックすると、即座にLoading...を表示し、読み込み終わると"Loaded"に変わります(望ましい動作)。
  • 3番目の"Load third"をクリックすると、即座にスピナーをオーバーレイ表示し、読み込み終わるとスピナーが消えます(望ましい動作)。

本記事では、私がある顧客のプロジェクトで実際に使ったシンプルな手法を紹介したいと思います。

参考: Get a SaaS From Zero to Customers in One Month | Rails Designer

以下のリポジトリでも本記事のすべてのコードを参照できます。このリポジトリには、遅いリクエストをsleepで真似たダミーのフレームコントローラも含まれています。

rails-designer-repos/turbo-busy - GitHub

それでは、私がこの問題にどうアプローチしたかを見ていきましょう。

🔗 1: Turbo Frame標準の「読み込み中」ステート表示

デフォルトのTurbo Frameは、ページを最初に表示したときの「読み込み中」ステート表示をシンプルな方法で処理します。
<turbo-frame>タグの内側に置いたコンテンツは、src属性のコンテンツを読み込んで置き換えるまで、表示され続けます。

以下は、リポジトリにある1番目のコード例です。

<%= link_to "Reload first", frame_path(content: "First frame reloaded...", id: "first", sleep: 2), data: { turbo_frame: "first" } %> <br>
<turbo-frame id="first" src="<%= frame_path(content: "First frame loaded...", id: "first", sleep: 2) %>">
  Loading...
</turbo-frame>

これは、ビューのページを最初に表示したときは完璧に機能します。ユーザーには"Loading..."が表示され、その後実際のコンテンツに置き換えられます。

しかしユーザーがlink_toをクリックすると問題が生じます。Turboはフレーム内の既存のコンテンツをいったん削除しますが、新しいコンテンツが配信されるまで空白のままになります。このような空白をそのままにしておくと、ユーザーエクスペリエンスが低下してしまいます。

🔗 2:「 読み込み中」インジケータをCSSだけで実現する

私はこの問題を解決するために、コンテンツの取得が終わるまでTurboが自動的にフレームに追加するaria-busy="true"属性を活用しています。この属性をCSSセレクタとして使うことで、既存の(古くなった)コンテンツの手前に"Loading..."インジケータをオーバーレイ表示できます。

これで、新しいコンテンツの読み込みが終わるまで古いコンテンツを消さずに表示しつつ、"Loading..."が手前に表示されるようになります(もちろん、表示のスタイルはCSSでいくらでも自由に設定できます)。

以下はそのためのCSSです。

#second {
  position: relative;
}

#second::after {
  content: "Loading...";
  position: absolute;
  inset: 0;
  padding: .125rem;
  display: none;
  background-color: #fff;
}

#second[aria-busy="true"]::after {
  display: block;
}

#secondのフレームはposition: relativeで指定できるので、その内側に::after疑似要素を配置できます。この::after疑似要素はオーバーレイとしてスタイル設定され、デフォルトでは非表示です。

これに加えて#second[aria-busy="true"]::afterを指定することでマジックが起きます。Turboがaria-busy="true"を設定するとこのCSSルールが有効になり、この疑似要素のdisplayblockに変わって、"Loading..."がオーバーレイ表示されるようになります。

🔗 3: SVGでスピナーを表示する

テキストベースの"Loading..."もよいのですが、スピナーのアニメーションの方が見た目がよいこともよくあります。
方法論は上と同じで、[aria-busy="true"]ステートでCSSドリブンのインジケータを表示します。主な違いは、::after疑似要素のスタイル設定にあります。

#third {
  position: relative;
}

#third::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 1rem; height: 1rem;
  display: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='currentColor'%3E%3Cpath d='M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-color: #fff;
  border-radius: 9999px;
  animation: spin 1.5s linear infinite;
}

#third[aria-busy="true"]::after {
  display: inline-block;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

ここではcontentを空にしています。スピナーにはdataとしてURIにエンコードしたSVG画像を使ってbackground-imageとして設定します。このspinアニメーションは疑似要素を回転させ続けます。先ほどと同様に、この疑似要素もaria-busy="true"属性がフレームに存在する間だけ表示されます。

🔗 4: rails_iconsのアイコンを使う

最後の例は、3番目の例を改良したものです。SVGをCSSに埋め込んでもいいのですが、私は自分のアイコン(🦉)をrails_icons gemで表示するのが好みです。さらにCSSカスタムプロパティを使えば、よりクリーンに書けます。

rails-designer/rails_icons - GitHub

まず、アイコンのSVGデータをビューからCSSに渡すときに、turbo-frame要素自身のstyle属性を使います。SVGはrails_icons gemで生成してからBase64エンコードします。

<%= link_to "Load fourth", frame_path(content: "Loaded. Did you see the spinner icon from Rails Icons?", id: "fourth", sleep: 2), data: {turbo_frame: "fourth"} %> <br>
<turbo-frame id="fourth" style="--icon: url('data:image/svg+xml;base64,<%= Base64.strict_encode64(icon("loader")) %>')">
</turbo-frame>
<p>Using <%= link_to "Rails Icons", "https://github.com/rails-designer/rails_icons" %></p>

補足

私はこれをrails_icon gemのファーストパーティ機能とするためのプルリクをオープンしました(#84)。rails_icon gemにマージしたい機能が他にありましたらお知らせください。

CSSは3番目の例と似ていますが、ハードコードされたdata URIをvar(--icon)に置き換えてあります。

#fourth {
  position: relative;
}

#fourth::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 1rem; height: 1rem;
  display: none;
  background-image: var(--icon);
  background-repeat: no-repeat;
  background-color: #fff;
  border-radius: 9999px;
  animation: spin 1.5s linear infinite;
}

#fourth[aria-busy="true"]::after {
  display: inline-block;
}

以上でできあがりです!本記事で紹介したテクニックは、さまざまな顧客の案件で実際にうまく動いています。私が改良したのは、フレームの読み込みやフォームの保存でコンテンツやスタイルをCSSだけで変更するようにしたことです。この改良を気に入っていただけたら、ぜひ私までお知らせください。

関連記事

Rails: マルチステップのフォームやウィザードをgemなしで構築する(翻訳)

Rails:繰り返しイベントの日付や期間を自然言語で指定できるパーサーをStimulusで実装する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。