はじめ
本記事は、Rails7、stimulusを使用した環境のもと、ActionTextをカスタマイズ(主に添付ファイルのアップロードまわり)する方法を紹介します。ActionTextは、trixを利用しており、これによってリッチテキストコンテンツと編集機能を簡単に導入することができます。デフォルトの挙動のまま使用する分には特に問題にはなりませんが、「こうしてほしい」という要望に応えるためにカスタマイズしていくためには、それなりの理解が必要であり、その前提のもと話を進めていきます。
参考
RailsガイドのActionTextに関する記事はこちら
ActiveStorageのAPIはこちら
trixのイベントはこちら
選択できるファイル形式を限定してほしい
<input type="file" accept="image/*">
的なことを、ActionTextの装飾ボタンにも適用する
<div class=data-controller="action-text">
<%= f.rich_text_area :description, data: { action: "trix-initialize->action-text#trixInitialize } %>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
trixInitialize() {
// 初期設定のpickFilesを上書きする
// FYI: https://www.npmjs.com/package/trix?activeTab=explore
Trix.config.input.pickFiles = () => {
const fileInput = document.createElement("input")
// image/jpeg, image/png, image/gifに限定
fileInput.setAttribute("type", "file")
fileInput.setAttribute("accept", "image/jpeg, image/png, image/gif")
fileInput.setAttribute("multiple", "true")
fileInput.dataset.action = "trix-file-accept->trix#uploadfile"
fileInput.addEventListener("change", () => {
const {files} = fileInput;
Array.from(files).forEach(insertAttachment)
});
fileInput.click()
}
const insertAttachment = (file) => {
const trixEditor = document.querySelector("trix-editor").editor;
trixEditor.insertFile(file);
}
}
}
ダイレクトアップロードを使わないでほしい
ActionTextは、デフォルトでjsからS3にダイレクトアップロードするようになっています。インフラ担当が別にいる場合は細かな設定がわからないため、IAMロールにポリシーを設定するお願いをしたりなど協力しながら動作確認を行わないといけないので意外と面倒な時があります。
一般的に、Rails経由でアップロードしたほうがセキュリティ的にも安全で自然だと思います。
<div class="form-control-container" data-controller="action-text" data-action-text-url-value="<%= admin_action_text_attachments_path %>">
<%= f.rich_text_area :description, data: { action: "trix-file-accept->action-text#trixFileAccept trix-attachment-add->action-text#trixAttachmentAdd" } %>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
url: String
}
// リクエストを送信するときに、Railsが発行したCSRFトークンも一緒に送るようにする。
connect() {
this.csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
trixAttachmentAdd(event) {
const attachment = event.attachment
const file = attachment.file
let formData = new FormData()
formData.append("Content-Type", file.type)
formData.append("attachment[file]", file)
formData.append("attachment[model]", this.modelValue)
if (file) {
let xhr = new XMLHttpRequest
xhr.open("POST", this.urlValue, true);
xhr.setRequestHeader("X-CSRF-Token", this.csrfToken);
xhr.upload.onprogress = function(event) {
const progress = event.loaded / event.total * 100;
attachment.setUploadProgress(progress);
}
xhr.onload = function() {
if (xhr.status === 201) {
const data = JSON.parse(xhr.responseText)
// actoin_text_attachments_controller.rbから返された値を属性に設定することでリッチテキスト内で画像が表示される
return attachment.setAttributes({
url: data.url,
sgid: data.signed_id
})
} else {
throw new Error(`upload failed. status: ${xhr.status}`)
}
}
return xhr.send(formData);
} else {
throw new Error("attachmet file does not exist")
}
}
}
class Admin::ActionTextAttachmentsController < Admin::ApplicationController
# s3のバケット直下にファイルを保存せず、ディレクトリを切って保存していく場合(bucket/admin/image.pngなど)、
# そのパスを返すメソッドをconcerns/に持っておくことで他のコントローラでも使用できるようする。
include ActiveStorage::UploadPath
def create
file = attachment_params[:file]
return render json: {}, status: :bad_request if file.blank?
# ActiveStorage::Blobのインスタンスが作成され保存されたのち、サービス上へアップロード
# upload_pathは ActiveStorage::UploadPathに定義されたメソッド
blob = ActiveStorage::Blob.create_and_upload!(key: upload_path, io: file,
filename: file.original_filename)
# ここではCloudFront経由のURLを返す独自のヘルパーcdn_urlを使用。
# リッチテキスト上にアップロードした画像を表示させるには、そのURLと新しく作成されたblobのsigned_idが必要
render json: { url: cdn_url(blob), signed_id: blob.signed_id }, status: :created
end
private
def attachment_params
params.fetch(:attachment, {}).permit(:file)
end
end
以上です。いい勉強になりました。