Search code examples
ruby-on-rails-4nested-formsnested-attributesform-helpers

Rails submit multiple nested forms with one submit button


I'm building a rails app for a competition leaderboard. The data model is

class Tournament
  has_many :events
end

class User
  has_many :entries
  has_many :events, through: :entries
end

class Events
  has_many :entries
  belongs_to :tournament
end

class Entry
  belongs_to :user
  belongs_to :event
end

Each tournament has 2 events and a user can enter up to 4 entries per event, so a total of 8 entries per tournament. I'd like to allow the user to enter all 8 entries in one form, with one submit button.

My routes

resources :entries, except: [:index, :show] do
  collection do
    match 'create_collection',  via: [:create]
  end
end

I also have a create_collection method in the Entries controller to process the entries, though I haven't got that far yet. I'm not sure how to get the form working properly.

_form.html.haml (which is a partial rendered by views/tournaments/index.html.haml and passes next_events which is the 2 event instances for the Tournament.

.entry-form
  = form_tag create_collection_entries_path do |form|
    - next_events.each do |event|
      = event.name
      = fields_for "events[]", event do |f|
        - 4.times do 
          = f.fields_for :entries do |f|
            = f.label_tag 'player'
            = f.text_field 'player'

    = submit_tag "Submit", class: "btn btn-success"

The form displays as I expect but when I click the submit button only the last 4 entries are submitted in the params and the event id is not being passed

"events"=>[{"entries_attributes"=>
  {"0"=>{"player"=>"player5","id"=>"33"},
   "1"=>{"player"=>"player6", "id"=>"34"},
   "2"=>{"player"=>"player7", "id"=>"35"},
   "3"=>{"player"=>"player8", "id"=>"36"}}}],
"commit"=>"Submit"}

How do I get all the entry parameters for both events to be submitted with the correct event-id?


Solution

  • My research indicates that rails doesn't provide a way to submit multiple instances of a model in one form with one submit. I looked at this and this which didn't quite work for me. So I rolled my own.

    The form to submits all the values in the params hash in a fairly manageable way. It's not the most elegant solution and requires a fair bit of processing in the back end. But I can't find a way to do this in a more railsy way.

    form.html.haml

    .entry-form.on
      = form_tag create_collection_entries_path(@user.id) do
        - Tournament.next.events.each do |event|
          %div= event.name
          %div
            - @num_of_entry_forms.times do |n|
              %div
                = label_tag 'player'
                = text_field_tag "event[#{event.id}][entries][player_#{n}]" 
    
        = submit_tag "Submit", class: "btn btn-success"
    

    routes.rb

    resources :entries, except: [:index, :show] do
      collection do
        get '/new_collection/:user_id', to: 'entries#new_collection', as: 'new_collection'
        post '/create_collection/:user_id', to: 'entries#create_collection', as: 'create_collection'
      end
    end
    

    entries_controller.rb

    def new_collection
      @num_of_entry_forms = EventEntryCollectionCreator::MAX_ENTRIES_PER_EVENT
      @user = User.find(params[:user_id])
    end
    
    def create_collection
      @eec = EventEntryCollection.new(params)
      if @eec.save
        redirect_to root_path
      else
        render :new_collection
      end
    end
    

    I'm still working on the EventEntryCollectionCreator which is a service model, but here's the core of it.

    MAX_ENTRIES_PER_EVENT = 4
    
    def save
      params["event"].each do |event|
        event.last["entries"]["player"].each do |entry|
          entry = Entry.new(event_id: event.first, user_id: user_id, player: entry.last)
          @entries_collection << entry
        end
      end
      save_collection
    end
    
    private
    def save_collection
      @entries_collection.each do |entry|
        if valid_entry?
          entry.save
          @valid_models << entry
        else
          @invalid_models << entry
        end
      end
    end
    

    As I said this isn't the most elegant solution but it solves the problem. The good points are that the messy backend processing is abstracted out of the controller into its own service model. The negative aspects are there are a few code smells, a lot of forcing values into hashes and objects having to know too much about the hash structure.

    I'm open to other solutions or improvements to this work in progress.