首页 > 解决方案 > Using JavaScript to submit an array and a file object to a Rails backend

问题描述

I haven't been able to figure out how to get my JavaScript to send a request in a format that Rails will accept when I try to edit a Game with a File parameter and an array parameter in the same payload.

The Rails controller looks like this (simplified, obviously):

class GamesController < ApplicationController
  def update
    @game = Game.find(params[:id])
    authorize @game

    respond_to do |format|
      if @game.update(game_params)
        format.html { render html: @game, success: "#{@game.name} was successfully updated." }
        format.json { render json: @game, status: :success, location: @game }
      else
        format.html do
          flash.now[:error] = "Unable to update game."
          render :edit
        end
        format.json { render json: @game.errors, status: :unprocessable_entity }
      end
    end
  end

  private

  def game_params
    params.require(:game).permit(
      :name,
      :cover,
      genre_ids: [],
      engine_ids: []
    )
  end
end

So I have JavaScript like so:

// this.game.genres and this.game.engines come from
// elsewhere, they're both arrays of objects. These two
// lines turn them into an array of integers representing
// their IDs.
let genre_ids = Array.from(this.game.genres, genre => genre.id);
let engine_ids = Array.from(this.game.engines, engine => engine.id);

let submittableData = new FormData();
submittableData.append('game[name]', this.game.name);
submittableData.append('game[genre_ids]', genre_ids);
submittableData.append('game[engine_ids]', engine_ids);
if (this.game.cover) {
  // this.game.cover is a File object
  submittableData.append('game[cover]', this.game.cover, this.game.cover.name);
}

fetch("/games/4", {
  method: 'PUT',
  body: submittableData,
  headers: {
    'X-CSRF-Token': Rails.csrfToken()
  },
  credentials: 'same-origin'
}).then(
  // success/error handling here
)

The JavaScript runs when I hit the submit button in a form, and is supposed to convert the data into a format Rails' backend will accept. Unfortunately, I'm having trouble getting it to work.

I'm able to use JSON.stringify() instead of FormData for submitting the data in the case where there's no image file to submit, like so:

fetch("/games/4", {
  method: 'PUT',
  body: JSON.stringify({ game: {
    name: this.game.name,
    genre_ids: genre_ids,
    engine_ids: engine_ids
  }}),
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': Rails.csrfToken()
  },
  credentials: 'same-origin'
})

This works fine. But I haven't been able to figure out how to use JSON.stringify when submitting a File object. Alternatively, I can use a FormData object, which works for simple values, e.g. name, as well as File objects, but not for array values like an array of IDs.

A successful form submit with just the ID arrays (using JSON.stringify) looks like this in the Rails console:

Parameters: {"game"=>{"name"=>"Pokémon Ruby", "engine_ids"=>[], "genre_ids"=>[13]}, "id"=>"4"}

However, my current code ends up with something more like this:

Parameters: {"game"=>{"name"=>"Pokémon Ruby", "genre_ids"=>"18,2,15", "engine_ids"=>"4,2,10"}, "id"=>"4"}

Unpermitted parameters: :genre_ids, :engine_ids

Or, if you also upload a file in the process:

Parameters: {"game"=>{"name"=>"Pokémon Ruby", "genre_ids"=>"13,3", "engine_ids"=>"5", "cover"=>#<ActionDispatch::Http::UploadedFile:0x00007f9a45d11f78 @tempfile=#<Tempfile:/var/folders/2n/6l8d3x457wq9m5fpry0dltb40000gn/T/RackMultipart20190217-31684-1qmtpx2.png>, @original_filename="Screen Shot 2019-01-27 at 5.26.23 PM.png", @content_type="image/png", @headers="Content-Disposition: form-data; name=\"game[cover]\"; filename=\"Screen Shot 2019-01-27 at 5.26.23 PM.png\"\r\nContent-Type: image/png\r\n">}, "id"=>"4"}

Unpermitted parameters: :genre_ids, :engine_ids

TL;DR: My question is, how can I send this payload (a name string, an array of IDs, as well as a game cover image) to Rails using JavaScript? What format will actually be accepted and how do I make that happen?


The Rails app is open source if that'd help at all, you can see the repo here. The specific files mentioned are app/controllers/games_controller.rb and app/javascript/src/components/game-form.vue, though I've simplified both significantly for this question.

标签: javascriptruby-on-railsruby-on-rails-5form-datarails-activestorage

解决方案


I figured out that I can do this using ActiveStorage's Direct Upload feature.

In my JavaScript:

// Import DirectUpload from ActiveStorage somewhere above here.
onChange(file) {
  this.uploadFile(file);
},
uploadFile(file) {
  const url = "/rails/active_storage/direct_uploads";
  const upload = new DirectUpload(file, url);

  upload.create((error, blob) => {
    if (error) {
      // TODO: Handle this error.
      console.log(error);
    } else {
      this.game.coverBlob = blob.signed_id;
    }
  })
},
onSubmit() {
  let genre_ids = Array.from(this.game.genres, genre => genre.id);
  let engine_ids = Array.from(this.game.engines, engine => engine.id);
  let submittableData = { game: {
    name: this.game.name,
    genre_ids: genre_ids,
    engine_ids: engine_ids
  }};

  if (this.game.coverBlob) {
    submittableData['game']['cover'] = this.game.coverBlob;
  }

  fetch(this.submitPath, {
    method: this.create ? 'POST' : 'PUT',
    body: JSON.stringify(submittableData),
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': Rails.csrfToken()
    },
    credentials: 'same-origin'
  })
}

I then figured out that, with the way DirectUpload works, I can just send the coverBlob variable to the Rails application, so it'll just be a string. Super easy.


推荐阅读