概要
ActiveStorageでやればいいところ、
- 別モデルのフォーム内
- ファイル選択と同時に登録処理
と言う条件だったためどうせJS制御になるしと言うことで
Ajaxを使って複数ファイルを送ることにしたけど、
複数ファイル選択だったところにつまずいたので備忘録です。
環境
- Ruby: 3.1.0
- Rails: 6.1.4.4
実装
class User < ApplicationRecord
has_many :user_avatar_files
end
class UserAvatarFiles < ApplicationRecord
belongs_to :user
has_one_attached :avatar
end
= form_with model: @user, url: users_path, multipart: true, method: :post do |f|
= hidden_field_tag "authenticity_token", form_authenticity_toke
= hidden_field_tag "user_id", @user.id
= file_field_tag 'avatars[]', multiple: true, id: 'upload', class: 'js-avatar', hidden: true
label for="upload" class="btn"
| ファイルをアップロード
#avatar-template.is-hidden
.avatar-row
= link_to '', "#", class: 'avatar-link'
= link_to '削除', "#", class: 'avatar-delete', method: :delete
$(document).on('change', '.js-avatar', function (e) {
const files = $(this).prop('files');
const form_data = new FormData();
form_data.append('authenticity_token', $("#authenticity_token").val());
form_data.append('user_id', $('#user_id').val());
$.each(files, function(i, val) {
form_data.append('avatars[]', val);
});
$.ajax({
url: '/user_avater_files',
type: 'post',
data: form_data,
contentType: false,
processData: false
})
.done(function(data)
$.each(data.created_avatars,function(i, val) {
$('#avatar-template').children().clone(true).appendTo('.avatar-lists')
.removeClass('is-hidden')
.find('.avatar-delete').each(function() {
$(this).attr('href', val.delete_url);
}).end().find('.avatar-link').each(function() {
$(this).text(val.filename).attr('href', val.url);
})
})
})
});
def create
@user = User.find(params[:user_id])
created_avatars = []
params[:avatars].each do |avatar|
user_avater_file = @user.user_avater_file.create!
user_avater_file.avatar.attach(avatar)
avatar = user_avater_file.avatar
created_avatars.push({filename: avatar.filename, url: rails_blob_path(avatar), delete_url: users_avatar_path(user_avater_file.id)} )
end
return render json: {created_avatars: created_avatars}
end
解説
ファイルの複数選択
ファイルの複数選択はinput
にmultiple
のオプションをつけることで可能です。
今回の場合はfile_field_tag
なのでmultiple: true
の部分になります。
= file_field_tag 'avatars[]', multiple: true, id: 'upload', class: 'js-avatar', hidden: true
複数選択されたファイルの送信
選択済みのファイルが1つの時には$('セレクタ').prop('files')[0]
でアクセスすることができ、
送信時にもこれをFormData
に入れて渡してあげれば処理できます。
しかし今回は複数なので、一つ一つFormData
に入れてあげる必要があります。
const files = $(this).prop('files');
const form_data = new FormData();
$.each(files, function(i, val) {
form_data.append('avatars[]', val);
});
この部分で格納前にわざわざ配列を作って渡したりとかして無駄に詰まりました。
ファイルデータは一つ一つappendすれば良いようです。
あとは返ってきた値を使って
必要な表示をしてあげればOKです。
番外
postが送れない
Can't verify CSRF token authenticity.
こんな感じのエラーが出てコケました。
以下の部分が対策です。
= hidden_field_tag "authenticity_token", form_authenticity_toke
form_data.append('authenticity_token', $("#authenticity_token").val());
その他にcontrollerで制御するやり方や、
'meta[name="csrf-token"]
から取得するやり方もあります。
送られてきたデータが変
controllerに送られてきたデータがなんかおかしく、
------WebKitFormBoundaryGkAId8S6WOXgIw06
15:28:51 web.1 | Content-Disposition: form-data; name="resumes[]"; filename="ミッキー.jpeg"
15:28:51 web.1 | Content-Type: image/jpeg
15:28:51 web.1 |
15:28:51 web.1 | ����JFIF��� ( %!3!%)+...383.7(-.+
15:28:51 web.1 |
...
こんな感じで想定していたものになっていなかったのですが、
FormDataを使用する場合は
$.ajax({
url: '/user_avater_files',
type: 'post',
data: form_data,
contentType: false,
processData: false
})
このcontentType: false
とprocessData: false
がいるようです。
contentTypeをfalseにすることで、Content-Typeの指定をFormDataに任せるのと、
processDataをfalseにするのは、クエリ文字列への変換を無効化してくれるらしいです。
参考にさせていただきました記事の作者様、
ありがとうございました。