質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.61%

Railsの複数画像投稿機能に枚数制限をかけたいです。。。

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 101

score 3

前提・実現したいこと

Railsで複数画像投稿機能の実装をしたのですが、画像投稿枚数に制限をかけたいです。

該当のソースコード

document.addEventListener('DOMContentLoaded', function(){
  if ( document.getElementById('item-image')){
    const ImageList = document.getElementById('image-list');

    const createImageHTML = (blob) => {
      // 画像を表示するためのdiv要素を生成
      const imageElement = document.createElement('div');
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length

      // 表示する画像を生成
      const blobImage = document.createElement('img');
        blobImage.className="img-preview";
      blobImage.setAttribute('src', blob);

      // ファイル選択ボタンを生成
      const inputHTML = document.createElement('input')
      inputHTML.setAttribute('id', `item-image_${imageElementNum}`)
      inputHTML.setAttribute('name', 'post[images][]')
      inputHTML.setAttribute('type', 'file')

      // 生成したHTMLの要素をブラウザに表示させる
      imageElement.appendChild(blobImage);
      imageElement.appendChild(inputHTML)
      ImageList.appendChild(imageElement);

      inputHTML.addEventListener('change', (e) => {
        file = e.target.files[0];
        blob = window.URL.createObjectURL(file);

        createImageHTML(blob)
      })
    }

    document.getElementById('item-image').addEventListener('change', function(e){
      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);

      createImageHTML(blob);
    });
  }
});
<div class="post-form">
  <div class="post-container">
    <%= form_with(model: @post, local: true) do |f| %>
    <h3>sample</h3>
    <div class ="post-title">
      <%= f.text_field :title,class:"post-title-form", placeholder: "title" %>
    </div>
    <div class ="post-image">
      <%= f.file_field :images, multiple: true, name: 'post[images][]', class:"post-image-form", id:"item-image" %>
    </div>
    <div class="preview" id="image-list">

    </div>
    <div class = "post-text">
      <%= f.text_area :text,class:"post-text-form", placeholder: "text", rows: "10" %>
    </div>
    <div class = "post-prefecture">
      <%= f.collection_select(:prefecture_id, Prefecture.all, :id, :name, {}, {class:"post-select-box", id:"post-prefecture"}) %>
    </div>
      <%= f.submit "SEND", class:"post-white-btn" %>
    <% end %>
  </div>
</div>

Postコントローラー

class PostsController < ApplicationController
  before_action :authenticate_user!

  def index
    @posts = Post.order("created_at DESC").limit(5)
  end

  def new
    @post = Post.new
  end

  def create
    @post = Post.create(post_params)
    if @post.save
      redirect_to root_path
    else
      render :new
    end
  end

  def show
    @post = Post.find(params[:id])
    @comment = Comment.new
    @comments = @post.comments.includes(:user)
  end

  def edit
    @post = Post.find(params[:id])
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
    if post.save
      redirect_to root_path
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
    if post.destroy
      redirect_to root_path
    end
  end

  def prefecture
    @post = Post.find_by(prefecture_id: params[:id])
    @posts = Post.where(prefecture_id: params[:id]).order('created_at DESC')
  end


  private
  def post_params
    params.require(:post).permit(:title, :text, :prefecture_id, images: []).merge(user_id: current_user.id)
  end

Postモデル

class Post < ApplicationRecord
  validates :title, presence: true
  validates :text, presence: true
  validates :prefecture_id, numericality: { other_than: 1 , message: "can't be blank"} 
  validates :images, presence: true

  belongs_to :user
  has_many :comments
  has_many :likes, dependent: :destroy
  has_many :liking_users, through: :likes, source: :user
  has_many_attached :images
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to :prefecture
end

試したこと

https://qiita.com/tochisuke221/items/bff7d930ae282552ef77
この記事を参考に実装を試みたのですが、
うまく実装ができませんでした。

補足情報(FW/ツールのバージョンなど)

学習を始めたばかりで、初歩的な質問になってしまいますが、
教えていただけると幸いです。。。
追記するものなどあれば対応いたします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • shinoharat

    2021/09/15 18:47 編集

    コントローラーとモデルのコードも載せて貰えるとアドバイスしやすいです。

    なぜなら、JavaScript だけで制限をかけても、それは簡単にすり抜けられてしまうからです。
    JavaScript による枚数制限と、サーバ側(Rails)による枚数制限の両方を実装する必要があり、両方の解説が必要なので、サーバ側のコードも載せてください。

    キャンセル

  • naoki1128

    2021/09/15 18:55

    ご回答ありがとうございます。
    コントローラーとモデルのコードを追記いたしました。
    よろしくお願いいたします。

    キャンセル

回答 1

checkベストアンサー

+1

サーバ側(Rails)の枚数制限

まずはサーバ側で枚数制限を行います。
images.length で添付されているファイルの数を取得できるので、それをチェックするカスタムバリデーションメソッドを定義します。

class Post < ApplicationRecord
  FILE_NUMBER_LIMIT = 3

  validate :validate_number_of_files

  private

  def validate_number_of_files
    return if images.length <= FILE_NUMBER_LIMIT
    errors.add(:images, "に添付できる画像は#{FILE_NUMBER_LIMIT}件までです。")
  end
end

作ったら、画面から動きを確認してください。
ファイルを4件以上添付して SEND ボタンを押下すると、エラーメッセージが表示されるはずです。

--

セキュリティ上の理由により、サーバ側(rails側)のバリデーションは必ず実装する必要があります。
JavaScript でのバリデーションは、ちょっと知識のある人がその気になれば、簡単にすり抜けできてしまうからです。

(質問者さんが参考にされている Qiita の記事は、 JavaScript でしかチェックしていないので危険です)

クライアント側(JavaScript)の枚数制限

利用者からすれば、 ​SEND ボタンを押す前に枚数制限が分かった方が便利なので、 JavaScript 側で規定枚数までしか添付できないようにします。
しつこいようですが、 JavaScript でのバリデーションはあくまで単なる入力補助です。

--

本題に入ります。

「無条件でファイル選択ボタンを生成」だった処理を、
画面上のファイル選択ボタンが上限に達していないなら、ファイル選択ボタンを生成」という処理に変更します。

そのため、まずはファイル選択ボタンを生成している処理だけを1箇所に集めます。

-     // ファイル選択ボタンを生成
-     const inputHTML = document.createElement('input')
-     inputHTML.setAttribute('id', `item-image_${imageElementNum}`)
-     inputHTML.setAttribute('name', 'post[images][]')
-     inputHTML.setAttribute('type', 'file')

      // 生成したHTMLの要素をブラウザに表示させる
      imageElement.appendChild(blobImage);
-     imageElement.appendChild(inputHTML);
      ImageList.appendChild(imageElement);

-     inputHTML.addEventListener('change', (e) => {
-       file = e.target.files[0];
-       blob = window.URL.createObjectURL(file);
-
-       createImageHTML(blob)
-     })

+     // ファイル選択ボタンを生成
+     const inputHTML = document.createElement('input')
+     inputHTML.setAttribute('id', `item-image_${imageElementNum}`)
+     inputHTML.setAttribute('name', 'post[images][]')
+     inputHTML.setAttribute('type', 'file')
+
+     imageElement.appendChild(inputHTML)
+
+     inputHTML.addEventListener('change', (e) => {
+       file = e.target.files[0];
+       blob = window.URL.createObjectURL(file);
+
+       createImageHTML(blob, e.target)
+     })

--

「画面上のファイル選択ボタンが上限に達していないなら」の条件を付け加えます。

-     // ファイル選択ボタンを生成      
+     // 画面上のファイル選択ボタンが上限に達していないなら、ファイル選択ボタンを生成
+     const imageInputNum = document.querySelectorAll('input[name="post[images][]"]').length;
+     const limit = 3;
+     if(imageInputNum < limit) {
        const inputHTML = document.createElement('input')
        inputHTML.setAttribute('id', `item-image_${imageElementNum}`)
        inputHTML.setAttribute('name', 'post[images][]')
        inputHTML.setAttribute('type', 'file')

        imageElement.appendChild(inputHTML)

        inputHTML.addEventListener('change', (e) => {
          file = e.target.files[0];
          blob = window.URL.createObjectURL(file);

          createImageHTML(blob)
        })
+     }

ちなみに、 const limit = 3; と直接数値を書くのではなく、 Rails の Post::FILE_NUMBER_LIMIT を使い回す方がよりベターです。
Rails の値を js に渡す方法は色々ありますので、余力があれば調べてみてください。

(今回のケースだと「カスタムデータ属性」なんかが使いやすいのではと思います)

プレビュー表示の修正

この時点で動作を確認すると、選択ボタンが3つまでしか表示されないのが分かると思います。
ただし、プレビュー画像はどんどん増えていってしまうため、次はこちらを解決します。

--

「無条件で表示用divと画像を生成」だった処理を、
押されたボタンに対応した表示用divと画像が既に存在するなら、画像だけを更新。
そうでないなら、表示用divと画像を生成」という処理に変更します。

まずは、押されたボタンのID(e.target.id)を blobImage に覚えさせておきます。
以下の例では、カスタムデータ属性というものを使っています。ご存知ない場合は調べてみてください。
ちなみに for の部分は変数名みたいなものなので、何でも好きな名前を付けられます。

      const blobImage = document.createElement('img');
      blobImage.className="img-preview";
+     blobImage.dataset.for = target.id;
      blobImage.setAttribute('src', blob);

先程追加したカスタムデータ属性を使って、「既に画像が存在するかどうか」を調べます。
既に存在するなら、 createElement はせずに、画像の src だけ更新して処理を終了させます。

+     // 既に画像表示エリアが存在するなら img の src だけ更新
+     let preview = document.querySelector(`img[data-for="${target.id}"]`);
+     if (preview) {
+       preview.setAttribute('src', blob);
+       return;
+     }

preview が存在しない場合は、いままで通り、「画像を表示するためのdiv要素」と「表示する画像」を createElement します。

最終的な完成形は以下のようになります。

-   const createImageHTML = (blob) => {
+   const createImageHTML = (blob, target) => {

+     // 既に画像表示エリアが存在するなら img の src だけ更新
+     let preview = document.querySelector(`img[data-for="${target.id}"]`);
+     if (preview) {
+       preview.setAttribute('src', blob);
+       return;
+     }

      // 画像を表示するためのdiv要素を生成
      const imageElement = document.createElement('div');
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length

      // 表示する画像を生成
      const blobImage = document.createElement('img');
      blobImage.className="img-preview";
+     blobImage.dataset.for = target.id;
      blobImage.setAttribute('src', blob);

      ...中略...

-         createImageHTML(blob)
+         createImageHTML(blob, e.target)

      ...中略...

-     createImageHTML(blob);
+     createImageHTML(blob, e.target);

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/09/16 07:14

    丁寧な解説ありがとうございます!!
    とてもわかりやすく、思い通りの実装をする事ができました!
    本当にありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.61%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る