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

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

新規登録して質問してみよう
ただいま回答率
85.35%
HTML5

HTML5 (Hyper Text Markup Language、バージョン 5)は、マークアップ言語であるHTMLの第5版です。

Ruby on Rails

Ruby on Railsは、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

コードレビュー

コードレビューは、ソフトウェア開発の一工程で、 ソースコードの検査を行い、開発工程で見過ごされた誤りを検出する事で、 ソフトウェア品質を高めるためのものです。

HTML

HTMLとは、ウェブ上の文書を記述・作成するためのマークアップ言語のことです。文章の中に記述することで、文書の論理構造などを設定することができます。ハイパーリンクを設定できるハイパーテキストであり、画像・リスト・表などのデータファイルをリンクする情報に結びつけて情報を整理します。現在あるネットワーク上のほとんどのウェブページはHTMLで作成されています。

Q&A

解決済

1回答

3697閲覧

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

naoki1128

総合スコア3

HTML5

HTML5 (Hyper Text Markup Language、バージョン 5)は、マークアップ言語であるHTMLの第5版です。

Ruby on Rails

Ruby on Railsは、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

コードレビュー

コードレビューは、ソフトウェア開発の一工程で、 ソースコードの検査を行い、開発工程で見過ごされた誤りを検出する事で、 ソフトウェア品質を高めるためのものです。

HTML

HTMLとは、ウェブ上の文書を記述・作成するためのマークアップ言語のことです。文章の中に記述することで、文書の論理構造などを設定することができます。ハイパーリンクを設定できるハイパーテキストであり、画像・リスト・表などのデータファイルをリンクする情報に結びつけて情報を整理します。現在あるネットワーク上のほとんどのウェブページはHTMLで作成されています。

1グッド

1クリップ

投稿2021/09/15 09:17

編集2021/09/15 09:54

前提・実現したいこと

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

該当のソースコード

js

1document.addEventListener('DOMContentLoaded', function(){ 2 if ( document.getElementById('item-image')){ 3 const ImageList = document.getElementById('image-list'); 4 5 const createImageHTML = (blob) => { 6 // 画像を表示するためのdiv要素を生成 7 const imageElement = document.createElement('div'); 8 imageElement.setAttribute('class', "image-element") 9 let imageElementNum = document.querySelectorAll('.image-element').length 10 11 // 表示する画像を生成 12 const blobImage = document.createElement('img'); 13 blobImage.className="img-preview"; 14 blobImage.setAttribute('src', blob); 15 16 // ファイル選択ボタンを生成 17 const inputHTML = document.createElement('input') 18 inputHTML.setAttribute('id', `item-image_${imageElementNum}`) 19 inputHTML.setAttribute('name', 'post[images][]') 20 inputHTML.setAttribute('type', 'file') 21 22 // 生成したHTMLの要素をブラウザに表示させる 23 imageElement.appendChild(blobImage); 24 imageElement.appendChild(inputHTML) 25 ImageList.appendChild(imageElement); 26 27 inputHTML.addEventListener('change', (e) => { 28 file = e.target.files[0]; 29 blob = window.URL.createObjectURL(file); 30 31 createImageHTML(blob) 32 }) 33 } 34 35 document.getElementById('item-image').addEventListener('change', function(e){ 36 const file = e.target.files[0]; 37 const blob = window.URL.createObjectURL(file); 38 39 createImageHTML(blob); 40 }); 41 } 42});

html

1<div class="post-form"> 2 <div class="post-container"> 3 <%= form_with(model: @post, local: true) do |f| %> 4 <h3>sample</h3> 5 <div class ="post-title"> 6 <%= f.text_field :title,class:"post-title-form", placeholder: "title" %> 7 </div> 8 <div class ="post-image"> 9 <%= f.file_field :images, multiple: true, name: 'post[images][]', class:"post-image-form", id:"item-image" %> 10 </div> 11 <div class="preview" id="image-list"> 12 13 </div> 14 <div class = "post-text"> 15 <%= f.text_area :text,class:"post-text-form", placeholder: "text", rows: "10" %> 16 </div> 17 <div class = "post-prefecture"> 18 <%= f.collection_select(:prefecture_id, Prefecture.all, :id, :name, {}, {class:"post-select-box", id:"post-prefecture"}) %> 19 </div> 20 <%= f.submit "SEND", class:"post-white-btn" %> 21 <% end %> 22 </div> 23</div>

###Postコントローラー

ruby

1class PostsController < ApplicationController 2 before_action :authenticate_user! 3 4 def index 5 @posts = Post.order("created_at DESC").limit(5) 6 end 7 8 def new 9 @post = Post.new 10 end 11 12 def create 13 @post = Post.create(post_params) 14 if @post.save 15 redirect_to root_path 16 else 17 render :new 18 end 19 end 20 21 def show 22 @post = Post.find(params[:id]) 23 @comment = Comment.new 24 @comments = @post.comments.includes(:user) 25 end 26 27 def edit 28 @post = Post.find(params[:id]) 29 end 30 31 def update 32 post = Post.find(params[:id]) 33 post.update(post_params) 34 if post.save 35 redirect_to root_path 36 end 37 end 38 39 def destroy 40 post = Post.find(params[:id]) 41 post.destroy 42 if post.destroy 43 redirect_to root_path 44 end 45 end 46 47 def prefecture 48 @post = Post.find_by(prefecture_id: params[:id]) 49 @posts = Post.where(prefecture_id: params[:id]).order('created_at DESC') 50 end 51 52 53 private 54 def post_params 55 params.require(:post).permit(:title, :text, :prefecture_id, images: []).merge(user_id: current_user.id) 56 end

###Postモデル

ruby

1class Post < ApplicationRecord 2 validates :title, presence: true 3 validates :text, presence: true 4 validates :prefecture_id, numericality: { other_than: 1 , message: "can't be blank"} 5 validates :images, presence: true 6 7 belongs_to :user 8 has_many :comments 9 has_many :likes, dependent: :destroy 10 has_many :liking_users, through: :likes, source: :user 11 has_many_attached :images 12 extend ActiveHash::Associations::ActiveRecordExtensions 13 belongs_to :prefecture 14end

試したこと

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

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

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

shinoharat👍を押しています

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

shinoharat

2021/09/15 09:48 編集

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

2021/09/15 09:55

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

回答1

0

ベストアンサー

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

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

rb

1class Post < ApplicationRecord 2 FILE_NUMBER_LIMIT = 3 3 4 validate :validate_number_of_files 5 6 private 7 8 def validate_number_of_files 9 return if images.length <= FILE_NUMBER_LIMIT 10 errors.add(:images, "に添付できる画像は#{FILE_NUMBER_LIMIT}件までです。") 11 end 12end

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

--

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

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

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

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

--

本題に入ります。

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

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

diff

1- // ファイル選択ボタンを生成 2- const inputHTML = document.createElement('input') 3- inputHTML.setAttribute('id', `item-image_${imageElementNum}`) 4- inputHTML.setAttribute('name', 'post[images][]') 5- inputHTML.setAttribute('type', 'file') 6 7 // 生成したHTMLの要素をブラウザに表示させる 8 imageElement.appendChild(blobImage); 9- imageElement.appendChild(inputHTML); 10 ImageList.appendChild(imageElement); 11 12- inputHTML.addEventListener('change', (e) => { 13- file = e.target.files[0]; 14- blob = window.URL.createObjectURL(file); 15- 16- createImageHTML(blob) 17- }) 18 19+ // ファイル選択ボタンを生成 20+ const inputHTML = document.createElement('input') 21+ inputHTML.setAttribute('id', `item-image_${imageElementNum}`) 22+ inputHTML.setAttribute('name', 'post[images][]') 23+ inputHTML.setAttribute('type', 'file') 24+ 25+ imageElement.appendChild(inputHTML) 26+ 27+ inputHTML.addEventListener('change', (e) => { 28+ file = e.target.files[0]; 29+ blob = window.URL.createObjectURL(file); 30+ 31+ createImageHTML(blob, e.target) 32+ })

--

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

diff

1- // ファイル選択ボタンを生成 2+ // 画面上のファイル選択ボタンが上限に達していないなら、ファイル選択ボタンを生成 3+ const imageInputNum = document.querySelectorAll('input[name="post[images][]"]').length; 4+ const limit = 3; 5+ if(imageInputNum < limit) { 6 const inputHTML = document.createElement('input') 7 inputHTML.setAttribute('id', `item-image_${imageElementNum}`) 8 inputHTML.setAttribute('name', 'post[images][]') 9 inputHTML.setAttribute('type', 'file') 10 11 imageElement.appendChild(inputHTML) 12 13 inputHTML.addEventListener('change', (e) => { 14 file = e.target.files[0]; 15 blob = window.URL.createObjectURL(file); 16 17 createImageHTML(blob) 18 }) 19+ }

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

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

プレビュー表示の修正

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

--

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

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

diff

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

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

diff

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

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

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

diff

1- const createImageHTML = (blob) => { 2+ const createImageHTML = (blob, target) => { 3 4+ // 既に画像表示エリアが存在するなら img の src だけ更新 5+ let preview = document.querySelector(`img[data-for="${target.id}"]`); 6+ if (preview) { 7+ preview.setAttribute('src', blob); 8+ return; 9+ } 10 11 // 画像を表示するためのdiv要素を生成 12 const imageElement = document.createElement('div'); 13 imageElement.setAttribute('class', "image-element") 14 let imageElementNum = document.querySelectorAll('.image-element').length 15 16 // 表示する画像を生成 17 const blobImage = document.createElement('img'); 18 blobImage.className="img-preview"; 19+ blobImage.dataset.for = target.id; 20 blobImage.setAttribute('src', blob); 21 22 ...中略... 23 24- createImageHTML(blob) 25+ createImageHTML(blob, e.target) 26 27 ...中略... 28 29- createImageHTML(blob); 30+ createImageHTML(blob, e.target);

投稿2021/09/15 18:47

shinoharat

総合スコア1685

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

naoki1128

2021/09/15 22:14

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問