5つ星のレーティング機能をesbuildで作成したいあなたに

# 初めに

Webアプリなどで、レーティング機能を使ってユーザーが求めるコンテンツを提供しやすくしたいですよね？
GoogleMapや食べログなどで使われる「★★★」などを使ってコンテンツを評価する機能は、下記の効果を発揮するのに役に立ちます
・評価項目を5つに分けて集計結果をレーダーチャートにまとめることができる
・それぞれの項目でランキング化させることで、コンテンツの特徴を明確にできる

今回は各レシピのコーナーにレーティングを実装し、下記の機能ができていることを目標にします

<strong>・口コミ投稿をする際に、星レビュー機能を実装して評価できること
・口コミ投稿すると、星レビューの結果が保存されること
・口コミ投稿の詳細を見ると、星レビューが確認できること
・編集を押下すると、星レビューを修正し、保存できること
・各レシピの口コミ投稿星レビューが総合平均値として表示されること</storng>

星レビューは下記のように投稿機能同様1人のユーザーが多くの評価をするので、<strong>1対多</strong>の関係になります
また、レーティングはユーザーキーと投稿キーがIDとして紐づきます


:::note warn
補足
※ここでは口コミ投稿ですが、アプリによっては掲示板やつぶやきとして表現することもあるので、各自で編集してみてください
:::
[![Image from Gyazo](https://i.gyazo.com/5589dec1246f6e11f3e3305e09099e9a.png)](https://gyazo.com/5589dec1246f6e11f3e3305e09099e9a)

# 使用している環境(および注意点)
環境が異なっている場合、異なる対応になる可能性がございますので
ご注意ください

下記の環境及び説明にて、間違い及び訂正箇所ございましたら、ご連絡のほどよろしくお願いいたします

・バックエンド Rails 7.2.1, (ruby 3.2.3 (2024-01-18 revision 52bb2ac0a6) [x86_64-linux])

・インフラ fly.io,Google Cloud

・認証 Sorcery

・開発環境 Docker

# 実装の際に参考にした資料等
https://zenn.dev/goldsaya/articles/2746dd2b886be0
https://qiita.com/d0ne1s/items/10c2c2c05449b73b099d
https://qiita.com/Ryo-0131/items/63e81b13b9382690bee4

:::note warn
注意
私も勘違いおよび見逃していましたが、どのフロントエンド環境構築するかによって異なります
上記の資料は 
<strong>Webpackerを用いた5つ星のレーティング機能です</strong>
自分自身が使っているフロントエンド環境を確認しないと、思わぬところで躓く恐れがございますので実装する前に確認をしましょう
:::

https://github.com/wbotelhos/raty

:::note warn
注意
公式はDeepLで訳しても説明がざっくりしているので、鵜呑みするのは要注意です
あくまで参考にしていきましょう
:::
## 事前準備
### ・jQueryのインストール
rails6以降は、yarnでjqueryをインストールします
```
yarn add jquery
```
### ・設定ファイルで、jQueryを管理下に追加するための記述を追加
webpackerではconfig/webpack/environment.jsに追加していますが、esbuildを使用しているので、
app/javascript/application.jsに記載しております
```js
// jQueryを管理下に追加するための記述を追加
import $ from 'jquery';
window.$ = $;
window.jQuery = $;
// ここまで
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
```
esbuildを使っている場合、jQueryを管理下に追加するためには、JavaScriptのエントリーポイントファイルに直接インポートする形で追加するのが一般的です


## ・raty.jsの導入

https://github.com/wbotelhos/raty

公式githubよりダウンロードし、raty/src/imagesの中にある下記３つのスターの画像を自分のapp/assets/imagesフォルダに格納します
1\.公式のSSHをコピー
[![Image from Gyazo](https://i.gyazo.com/141b75af3178a5ff43d0fdd9018595ba.png)](https://gyazo.com/141b75af3178a5ff43d0fdd9018595ba)
2\.ターミナルでgit cloneを実行
```
git clone git@github.com:wbotelhos/raty.git
```
3\.ターミナルでgit cloneを実行
```
your_gitname@your_name:~$ cd raty
your_gitname@your_name:~/raty$ pwd
/home/your_gitname/raty
your_gitname@your_name:~/raty$ code /home/your_gitname/raty
```
4\.必要な画像を自分のapp/assets/imagesフォルダに格納
今回は小数点で平均値を出すために下記の画像を取り込みます
star-half.png
star-on.png
star-off.png

## ・application.jsにraty情報を追加
webpackerの場合は下記の認識ですが、
app/javascript/packs/application.js
```js
import Raty from "raty.js"
window.raty = function(elem,opt) {
  let raty =  new Raty(elem,opt)
  raty.init();
  return raty;
}
```
https://www.jsdelivr.com/package/npm/raty-js

こちらの資料のscriptとhrefをapp/views/layouts/application.html.erbに格納します
Railsのapplication.html.erbファイルは、全てのページで共有される共通のレイアウトを定義するためのファイルです。
今回の星レビューのJavascriptも全てのページで共有できるようにするため、公式のscriptとhrefを下記に格納します
```html
<!DOCTYPE html>
<html>
  <head>
    <!-- 以下省略 -->
    <!-- meta_tag -->
    <%= display_meta_tags %>
    <!-- 下記に格納 -->
    <script src="https://cdn.jsdelivr.net/npm/raty-js@4.3.0/build/raty.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/raty-js@4.3.0/src/raty.min.css" rel="stylesheet">
  </head>
  <!-- 以下省略 -->
```
app/views/layouts/application.html.erbの中で，raty-jsも「app/views/layouts/application.html.erb」ファイルのheadタグ内で直接CDNのスクリプトを読み込ませることができます


:::note warn
注意
src/raty.jsを自分のapp/javascriptにコピーするよう記事で掲載されていますが、この対応は省いてください
##### JavaScriptの変数ratyが二重に宣言するため、下記のエラーが発生します
```
Uncaught SyntaxError: Failed to execute 'replaceWith' on 'Element': missing ) after argument list
    at PageRenderer.renderElement (application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:4210:21)
    at PageRenderer.assignNewBody (application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:4350:16)
    at application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:4276:18
    at Bardo.preservingPermanentElements (application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:1965:11)
    at PageRenderer.preservingPermanentElements (application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:2043:17)
    at PageRenderer.replaceBody (application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:4274:16)
    at PageRenderer.render (application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:4236:18)
    at PageView.renderSnapshot (application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:1819:20)
    at PageView.render (application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:1785:20)
    at async application-bcf9c75303f411d1f8a00567e2b980880d5141192e51d7a200b17554f1bd9fe3.js:2790:7
```
このエラーの影響で
##### リロードしないと結果が出ない、もしく表示されないという不具合が起きます
:::
## ・投稿フォームに星レビュー機能の追加
1.　"star"カラムの追加
```
rails g migration AddStarToPosts star:float
```
これでpostテーブルにfloat型のstarカラムの追加用マイグレーションファイルが作成します
小数点の平均値で表示したい場合、float型ですが
整数値の場合はString型になります
各自の判断で決めていきましょう
```
rails db:migrate
```
2."star"カラムを保存できるようにデータ操作を許可します
postsコントローラーのストロングパラメーターへstarカラムを追記します
```Ruby
class PostsController < ApplicationController
# 省略
　private
　def post_params
      params.require(:post).permit(:title, :body, :post_image, :post_image_cache, :star)
　end
end
```
## 口コミ投稿と編集するためのview(html)の記述を行う
口コミ投稿のフォームに星レビューのフォームを追加します
app/view/posts/edit.html.erb
```erb
    <div class="col-lg-8 offset-lg-2">
     <!-- render機能で投稿機能を紐づけているので、おのずとレビュー機能も追加されます-->
      <%= render 'form', post: @post %>
    </div>
```
app/view/posts/_form.html.erb
```erb
<%= form_with model: @post do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
    <!--省略 -->
    <!-- 評価表示部位 -->
    <div class="mb-3">
      <%= f.label "評価" %>
      <%= @post.star %>
　<!-- render機能で星レビュー機能を紐づけてます-->
      <%= render 'star', post: @post %>
    </div>
    <%= f.submit nil, class: "btn btn-primary" %>
<% end %>
```
app/view/posts/_star.html.erb
```Ruby
<div data-raty data-score="<%= @post.star || 0 %>" data-read-only="false" data-score-name="post[star]"></div> # ビューでの適切なデータ属性設定
          
            <script>
             document.querySelector('[data-raty]').innerHTML = ''; # innerHTMLのクリア
             let opt = { # 
             starOn: "<%= asset_path('star-on.png') %>",
             starOff: "<%= asset_path('star-off.png') %>",
             starHalf: "<%= asset_path('star-half.png') %>",
             scoreName: 'post[star]' # 保存するモデル名[カラム名]の記述。
             };
             const raty = new Raty(document.querySelector('[data-raty]'),opt); # 公式のコードを追加及び修正
             raty.init(); # 公式のコードを追加
            </script>
```
<details><summary>解説</summary>

### ・ビューでの適切なデータ属性設定
各ページが必要な情報（スコア、読み取り専用か、スコア名）を data-* 属性で渡すことで、初期化関数が動的に動作を切り替えます

### ・innerHTMLのクリア
document.querySelector('[data-raty]').innerHTML = ''; で重複した星を回避します
この記載が抜けていると繰り返し処理をしているので、ブラウザバックで星評価のある画面に戻った際、星5個が星10個になるという不具合が発生します
[![Image from Gyazo](https://i.gyazo.com/92d6717aca154bfe53bc089eab7bd528.png)](https://gyazo.com/92d6717aca154bfe53bc089eab7bd528)


:::note warn
補足
empty()メソッドを使えば解消されるという記事が多くみられますが、確かにRaty.jsのインスタンスをクリアして再初期化することができます。
##### ただし、この方法はバグの根本的な原因を解決するものではないかもしれませんので、各自判断する必要があります
:::

### ・保存するモデル名[カラム名]の記述
保存するモデル名[カラム名]の記述し、Rubyの記述を行なっていく際は、
""で囲って使用します

### ・ 公式のコードを追加及び修正
https://github.com/wbotelhos/raty
下記の公式を引用して、追加及び修正します
```
const raty = new Raty(document.querySelector('[data-raty]'));

raty.init();
```
</details>

## 口コミ詳細のview(html)の記述を行う
口コミ詳細のフォームに星レビューのフォームを追加します
```erb
             <!-- 5つ星を評価を設置 -->
            <div data-raty></div>
             <script src="https://cdn.jsdelivr.net/npm/raty-js@4.3.0/build/raty.min.js"></script>
              <script>
               document.querySelector('[data-raty]').innerHTML = '';
               let showOpt = {
                starOn: "<%= asset_path('star-on.png') %>",
                starOff: "<%= asset_path('star-off.png') %>",
                starHalf: "<%= asset_path('star-half.png') %>",
                scoreName: 'post[star]',
                score: "<%= @post.star %>", 
                readOnly: true 
               };
               const raty = new Raty(document.querySelector('[data-raty]'),showOpt);
               raty.init();
              </script>
              <!-- ここまで -->
```
<details><summary>解説</summary>

### ・innerHTMLのクリア
document.querySelector('[data-raty]').innerHTML = ''; で重複した星を回避します
この記載が抜けていると繰り返し処理をしているので、ブラウザバックで星評価のある画面に戻った際、星5個が星10個になるという不具合が発生します

[![Image from Gyazo](https://i.gyazo.com/92d6717aca154bfe53bc089eab7bd528.png)](https://gyazo.com/92d6717aca154bfe53bc089eab7bd528)


:::note warn
補足
empty()メソッドを使えば解消されるという記事が多くみられますが、確かにRaty.jsのインスタンスをクリアして再初期化することができます。
##### ただし、この方法はバグの根本的な原因を解決するものではないかもしれませんので、各自判断する必要があります
:::

### ・投稿したレビュー評価を保存する
```javascript
score: "<%= @post.star %>"
```
評価(score)を持ってくる必要があるので、こちらの記述で引っ張るようにします

### ・readOnly: true で閲覧のみの表示にする
readOnly: trueを記述することで、raty.jsを動かさないようにします

### ・ 公式のコードを追加及び修正
https://github.com/wbotelhos/raty
下記の公式を引用して、追加及び修正します
```javascript
const raty = new Raty(document.querySelector('[data-raty]'));

raty.init();
```

</details>

 ## 口コミ一覧のview(html)の記述を行う
口コミ一覧のフォームに星レビューのフォームを追加し、口コミの評価の総合平均値を出します
app/views/posts/index.html.erb
```html
<div data-raty data-score="<%= @average_star %>"></div>
          <p><span class="average-score-text"><%= @average_star.round(1) %></span> / 5.0</p>
          <script>
           document.addEventListener("turbo:load", function() {
            const ratyElement = document.querySelector("[data-raty]");
             if (ratyElement) {
              document.querySelector('[data-raty]').innerHTML = '';
              const score = parseFloat(ratyElement.dataset.score);
              const raty = new Raty(ratyElement, {
              starOn: "<%= asset_path('star-on.png') %>",
              starOff: "<%= asset_path('star-off.png') %>",
              starHalf: "<%= asset_path('star-half.png') %>",
              score: score,
              readOnly: true
             });
              raty.init();
             }
           });
          </script>
```
<details><summary>解説</summary>

### ・ビューでの適切なデータ属性設定
各ページが必要な情報（スコア、読み取り専用か、スコア名）を data-* 属性で渡すことで、初期化関数が動的に動作を切り替えます
```HTML
data-score="<%= @average_star %>"
```
: レシピ全体の口コミ平均値を表示します

```HTML
<p><span class="average-score-text"><%= @average_star.round(1) %></span> / 5.0</p>
```
: レシピ全体の口コミ平均値をfloat型(小数点表記の)数値で表示します

### ・turbo:loadにイベント設定し、リロードして更新を防ぐ
#### イベントの種類: DOMContentLoaded と turbo:load
誤ったコード
```HTML
          <script>
          # "DOMContentLoaded"が誤り
           document.addEventListener("DOMContentLoaded", function() {
            const ratyElement = document.querySelector("[data-raty]");
             if (ratyElement) {
              # 省略
             }
           });
          </script>
```
#### DOMContentLoaded
ページが初めて完全に読み込まれたとき一度だけ発火します。
Turboを使用している場合、Turboが部分的にページを更新したとき（いわゆる「ページ遷移」）には発火しません。
<strong>タイミング:</strong>
ページの初期読み込み時のみ処理が行われるため、ページ遷移や部分的な更新では再度処理が行われないため、リロードしない限りRatyの初期化処理が再実行されません
上記の誤ったコードを記述すると、結果<strong>下記の画面のようにリロードしないと反映されません</strong>


[![Image from Gyazo](https://i.gyazo.com/82e7eb8ea3c22a857a7e8fc1fb973c31.gif)](https://gyazo.com/82e7eb8ea3c22a857a7e8fc1fb973c31)

正しいコード
```HTML
          <script>
           document.addEventListener("turbo:load", function() {
            const ratyElement = document.querySelector("[data-raty]");
             if (ratyElement) {
              #　省略
             }
           });
          </script>
```
#### turbo:load
Turboフレームワークが部分的にページを更新したり、ナビゲーションが完了した際にも発火します。このため、リロード不要でRatyの初期化処理が正しく再実行されます。

<strong>タイミング:</strong>
turbo:loadを使うことで、Turboがページの一部を更新するたびに、Ratyの初期化処理が実行されるため、常に最新のデータを表示できるようになります
上記の正しいコードを記述すると、結果<strong>リロード不要でRatyの初期化処理が正しく再実行されます</strong>

[![Image from Gyazo](https://i.gyazo.com/06a5f819cde108f2783ede80e42a4e91.gif)](https://gyazo.com/06a5f819cde108f2783ede80e42a4e91)



### ・innerHTMLのクリア
document.querySelector('[data-raty]').innerHTML = ''; で重複した星を回避します
この記載が抜けていると繰り返し処理をしているので、ブラウザバックで星評価のある画面に戻った際、星5個が星10個になるという不具合が発生します

[![Image from Gyazo](https://i.gyazo.com/92d6717aca154bfe53bc089eab7bd528.png)](https://gyazo.com/92d6717aca154bfe53bc089eab7bd528)


:::note warn
補足
empty()メソッドを使えば解消されるという記事が多くみられますが、確かにRaty.jsのインスタンスをクリアして再初期化することができます。
##### ただし、この方法はバグの根本的な原因を解決するものではないかもしれませんので、各自判断する必要があります
:::

### ・投稿したレビュー評価を保存する
```javascript
score: "<%= @post.star %>"
```
評価(score)を持ってくる必要があるので、こちらの記述で引っ張るようにします

### ・readOnly: true で閲覧のみの表示にする
readOnly: trueを記述することで、raty.jsを動かさないようにします

### ・ 公式のコードを追加及び修正
https://github.com/wbotelhos/raty
下記の公式を引用して、追加及び修正します
```javascript
const raty = new Raty(document.querySelector('[data-raty]'));

raty.init();
```


</details>

## リロードしないで結果が出せるのを確認する

始めにで記載したとおり、各レシピのコーナーにレーティングを実装し、下記の機能ができていることを確認します
・口コミ投稿をする際に、星レビュー機能を実装して評価できること
・口コミ投稿すると、星レビューの結果が保存されること
・口コミ投稿の詳細を見ると、星レビューが確認できること
・編集を押下すると、星レビューを修正し、保存できること
・各レシピの口コミ投稿星レビューが総合平均値として表示されること

[![Image from Gyazo](https://i.gyazo.com/d24cef3985c48f3f3e8a9aeaaa65869d.gif)](https://gyazo.com/d24cef3985c48f3f3e8a9aeaaa65869d)
[![Image from Gyazo](https://i.gyazo.com/d4754b1f2684ddecd3e5bfe4ac4f01c1.gif)](https://gyazo.com/d4754b1f2684ddecd3e5bfe4ac4f01c1)
[![Image from Gyazo](https://i.gyazo.com/c633a6b6f11eb0c09c79aed018b0abf3.gif)](https://gyazo.com/c633a6b6f11eb0c09c79aed018b0abf3)
[![Image from Gyazo](https://i.gyazo.com/06a5f819cde108f2783ede80e42a4e91.gif)](https://gyazo.com/06a5f819cde108f2783ede80e42a4e91)

## 終わりに
MVPリリース時に実装したかったのですが、Javascriptの構造が把握できていなかったため、
<strong>上記の技術記事ではwebpackerディレクトリがなぜ私の環境にはないのか、
手動でwebpackerのディレクトリ作らないといけないのか</strong>
と焦り、挫折していました
本リリース中の年末にようやく星レビュー機能が実装できましたので、esbuild方式で星のレーティング機能を作成していて躓いている人たちの力になるべく今回新たに記事を作成しました
星レーティング機能はJavascriptだけでなく、HTMLとCSSで作成する方法、svgを用いた方法などもございますので、星デザインおよびレビュー評価の方法は個人の好みがございます
今回はその一例として参考になっていただければ助かります

ご質問などございましたら、ご連絡のほどよろしくお願いいたします
それではみなさん、よいお年を
2024/12/27