Home > other >  Rails: direct upload and create has_one_attached parent
Rails: direct upload and create has_one_attached parent

Time:10-10

I'm making an image library type thing in Rails and Vue, and I'm using DirectUpload to manage attachments.

# photo.rb

class Photo < ApplicationRecord
  has_one_attached :file
  # ...
end
# photos_controller.rb

class PhotosController < ApplicationController
  load_and_authorize_resource

  before_action :set_photo, only: %i[show update destroy]

  protect_from_forgery unless: -> { request.format.json? }

  def index
    @photo = current_user.photos.new

    render action: 'index'
  end

  def create
    @photo = current_user.photos.create(photo_params)
    render json: PhotoBlueprint.render(@photo, root: :photo)
  end

  # ...

  def photo_params
    params.require(:photo).permit(:id, :file)
  end
end
# photos/index.html.erb

<%= simple_form_for(@photo, url: photos_path(@photo), html: { multipart: true }) do |f| %>
  <%= f.file_field :file, multiple: true, direct_upload: true, style: 'display: none;' %>
<% end %>

<div id='photos-app'></div>
// UserFileLib.vue

<script>
  import { mapState, mapActions } from 'pinia'
  import { usePhotoStore } from '@stores/photo'
  import { DirectUpload } from '@rails/activestorage'

  export default {
    name: 'UserFileLib',

    computed: {
      ...mapState(usePhotoStore, [
        'photos'
      ]),
      url () {
        return document.getElementById('photo_file').dataset.directUploadUrl
      },
      input () {
        return document.getElementById('file-input')
      },
    },

    mounted () {
      this.getPhotos()
    },

    methods: {
      ...mapActions(usePhotoStore, [
        'addPhoto',
        'getPhotos',
      ]),
      activestorageURL (blob) {
        return `/rails/active_storage/blobs/redirect/${blob.signed_id}/${blob.filename}`
      },
      uploadToActiveStorage () {
        const file = this.input.files[0]
        const upload = new DirectUpload(file, this.url)

        upload.create((error, blob) => {
          if (error) {
            console.log(error)
          } else {
            const url = this.activestorageURL(blob)
            console.log(url)

            this.getPhotos()
          }
        })
      },
      openFileBrowser () {
        this.input.click()
      },
      formatSize (bytes) {
        return Math.round(bytes / 1000)
      }
    }
  }
</script>

<template>
  <div
    @click="openFileBrowser"
    >

    Click or drop files here
  </div>

  <input
    type="file"
    :multiple="true"
    @change="uploadToActiveStorage"
    id="file-input" />

  <div >
    <div
      
      v-for="image in photos"
      :key="image.id">

      <img :src="image.url" :alt="image.label" />

      <div >
        <strong>{{ image.label }}</strong>

        <br />

        {{ formatSize(image.size) }} kb
      </div>

      <div >
        &times;
      </div>
    </div>
  </div>
</template>

Now, the uploads work fine, the blob is stored correctly.

My issue is that a new Photo object is not created to wrap the attachment, meaning the uploads are lost in the system and have no parent record.

What am I doing wrong?

CodePudding user response:

I've solved this for anyone else looking for help. The logic is to create or update the parent record after the upload is done. I missed this in the official documentation.

upload.create((error, blob) => {
  if (error) {
    // Handle the error
  } else {
    // ** This is the way **
    // Add an appropriately-named hidden input to the form with a
    // value of blob.signed_id so that the blob ids will be
    // transmitted in the normal upload flow 
    // ** End of **
    //
    const hiddenField = document.createElement('input')
    hiddenField.setAttribute("type", "hidden");
    hiddenField.setAttribute("value", blob.signed_id);
    hiddenField.name = input.name
    document.querySelector('form').appendChild(hiddenField)
  }
})

Since I'm using Vue and Pinia I made a solution in keeping with that logic:

// UserImageLib.vue

uploadToActiveStorage (input, file) {
  const url = input.dataset.directUploadUrl
  const upload = new DirectUpload(file, url)

  upload.create((error, blob) => {
    if (error) {
      console.log(error)
    } else {
      const params = { [input.name]: blob.signed_id }
      this.createPhoto(params)
    }
  })
},
// stores/photo.js

addPhoto (payload) {
  this.photos.unshift(payload)
},
createPhoto (payload) {
  http.post(`/photos`, payload).then(res => {
    const photo = res.data.photo
    this.addPhoto(photo)
  })
},
  • Related