【フロントエンド学習記録】ActiveStorageを使ってブログ記事に画像を複数枚添付できるようにする

前回、ActiveStorage を使って画像を1枚添付するやり方について見ていきました。

maimux2x.hatenablog.com

今回はその続きで、ActiveStorage を使って複数枚の画像を添付できるようにしていきたいと思います。

まず、Rails 側を修正していきます。

# app/models/post.rb

class Post < ApplicationRecord
  has_many_attached :images # 変更

  # 省略

  validates :images, content_type: [ "image/png", "image/jpeg" ]
end

前回、モデルにバリデーションを指定していなかったため、アップロードできるファイルを pngjpeg のみに限定しました。

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
# 省略

private

  def post_params
    params.expect(post: [ :title, :body, images: [] ]) # image を images: [] に修正
  end
end
# app/views/posts/_post.json.jb

{
  id:         post.id,
  created_at: post.created_at,
  title:      post.title,
  body:       post.body,
  image_urls: post.images.map { rails_blob_url(it) } # 修正 / 画像が添付されていない場合は空の配列
}

続いてEmber.js 側の修正です。ベースはできているため複数枚の画像を選択してリクエストできるようにしていきます。

// web/app/components/post-form.js

import Component from '@glimmer/component';
import { DirectUpload } from '@rails/activestorage/src/direct_upload';
import { action } from '@ember/object';

export default class PostFormComponent extends Component {
  @action
  uploadImage(e) {
    this.args.post.images = [];

    for (const file of e.target.files) {
      const upload = new DirectUpload(
        file,
        'http://localhost:3000/rails/active_storage/direct_uploads',
        {},
      );

      upload.create((error, blob) => {
        if (error) {
          console.error(error.message);
        } else {
          this.args.post.images.push(blob.signed_id);
        }
      });
    }
  }
}

今回は複数枚の画像を選択してそれぞれの signed_id を Rails 側へリクエストする必要があります。そのために this.args.post.images の空の配列を用意して FileList を反復処理しながら this.args.post.images へ signed_id を詰めていきます。

そしてそれをリクエスト時に送るようにします。

// web/app/controllers/admin/posts/new.js

import Controller from '@ember/controller';
import { action } from '@ember/object';
import { service } from '@ember/service';
export default class AdminPostsNewController extends Controller {
  @service router;
  @service toast;
  @service session;
  @action
  async createPost(event) {
    event.preventDefault();
    const response = await fetch('http://localhost:3000/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${this.session.token}`,
      },
      body: JSON.stringify({
        post: {
          title: this.model.title,
          body: this.model.body,
          images: this.model.images, // images に修正
        },
      }),
    });

ロジック部分はこれで修正できました!

今度は画像選択ボタンの見た目を整えていきます。

現在の画像選択ボタンはこんな見た目です。

<input type="file" id="file" name="file" multiple {{on "change" this.uploadImage}} />

以下のように input タグに hidden 属性を付け、label タグの for 属性で hidden にした input を参照することで、bootstrap のボタンデザインを適用した状態でファイル選択ができます。

また、Rails 側でバリデーションを追加したためエラーがあった場合の対応もしています。

<!-- web/app/components/post-form.hbs -->

<input type="file" id="file" name="file" class="{{if @post.errors.images "is-invalid"}}" multiple {{on "change" this.uploadImage}} hidden />
<label for="file" class="btn btn-outline-primary">
  Choose image
</label>
{{#each @post.errors.images as |error|}}
  <div class="invalid-feedback">
    {{error}}
  </div>
{{/each}}

画像選択ボタンにデザインを当てたことで、何枚の画像を選択しているのかが表示されなくなっているのですが、今回は最終的にマークダウンで任意の箇所に画像を挿入できることを目指しているため、この状態で問題ありません。

後は画像の表示を複数枚対応に修正していきます。

<!-- web/app/components/post.hbs -->

<div class="body card-text">
  {{@post.body}}
  {{#each @post.image_urls as |url|}}
    <img src={{url}} alt="" class="img-fluid">
  {{/each}}
</div>

これで複数枚の画像を添付できるようにする修正が完了です!

まとめ

いきなり複数枚画像の添付を目指さず、まずは1枚添付できることを確認して修正をしたことで複数枚対応が進めやすくなりました。

次回はブログ記事の投稿をマークダウンでできるようにしていく部分を見ていきます。