【jQuery】Ajaxでmultipleのファイルを送信する

## 概要

ActiveStorageでやればいいところ、

* 別モデルのフォーム内
* ファイル選択と同時に登録処理

と言う条件だったためどうせJS制御になるしと言うことで
Ajaxを使って複数ファイルを送ることにしたけど、
複数ファイル選択だったところにつまずいたので備忘録です。

## 環境

* Ruby: 3.1.0
* Rails: 6.1.4.4

## 実装
```ruby:model
class User < ApplicationRecord
  has_many :user_avatar_files
end

class UserAvatarFiles < ApplicationRecord
  belongs_to :user
  has_one_attached :avatar
end
```

```slim:html
= 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

```

```JavaScript:js
$(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);
          })
    })
  })
});
```

```ruby:controller
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`の部分になります。
```slim:html
= file_field_tag 'avatars[]', multiple: true, id: 'upload', class: 'js-avatar', hidden: true
```
##### 複数選択されたファイルの送信
選択済みのファイルが1つの時には`$('セレクタ').prop('files')[0]`
でアクセスすることができ、
送信時にもこれを`FormData`に入れて渡してあげれば処理できます。
しかし今回は複数なので、一つ一つ`FormData`に入れてあげる必要があります。
```javascript:js
const files = $(this).prop('files');
const form_data = new FormData();

$.each(files, function(i, val) {
  form_data.append('avatars[]', val);
});
```
この部分で格納前にわざわざ配列を作って渡したりとかして無駄に詰まりました。
ファイルデータは一つ一つappendすれば良いようです。
<br>
あとは返ってきた値を使って
必要な表示をしてあげればOKです。

### 番外
##### postが送れない
Can't verify CSRF token authenticity.
こんな感じのエラーが出てコケました。
以下の部分が対策です。
```slim:html
= hidden_field_tag "authenticity_token", form_authenticity_toke
```
```javascript:js
form_data.append('authenticity_token', $("#authenticity_token").val());
```
その他にcontrollerで制御するやり方や、
`'meta[name="csrf-token"]`から取得するやり方もあります。

https://qiita.com/naberina/items/d3b14521e78e0daccdcd

##### 送られてきたデータが変
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を使用する場合は
```javascript:js
  $.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にするのは、クエリ文字列への変換を無効化してくれるらしいです。

https://blog.hello-world.jp.net/posts/javascript-2640

参考にさせていただきました記事の作者様、
ありがとうございました。