Joe Bloggos
Joe Bloggos

Reputation: 889

Rails create multiple rows from one form

I have a rails app that has building and floors models

class Building < ApplicationRecord
    has_many :floors
end

class Floor < ApplicationRecord
  belongs_to :building
end

In my building form I want to ask the user how many floors the building has, and then when the building is created I want to add that many floors.

So the form would look like so:

<%= form_with(model: building, local: true) do |form| %>
  <% if building.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(building.errors.count, "error") %> prohibited this building from being saved:</h2>

      <ul>
        <% building.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div class="field">
    <%= form.label :" How many floors does the building have" %>
    <%= form.number :floors %> * not sure how to do this
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

Note I don't want the number of floors saved on the building model, it just creates the number of floors the user specified.

So if I created a building called "Walts Place" and said it has 10 floors it would create: Walts Place with id:1, and 10 floors with the building_id of 1.

Does that make sense?

Your help is greatly appreciated.

Update:

ActiveRecord::Schema.define(version: 2019_07_30_093037) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "buildings", force: :cascade do |t|
    t.string "name"
    t.float "distancetocondensors"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "floors", force: :cascade do |t|
    t.bigint "building_id", null: false
    t.string "name"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["building_id"], name: "index_floors_on_building_id"
  end

  add_foreign_key "floors", "buildings"
end

Upvotes: 2

Views: 309

Answers (2)

Abhishek Aravindan
Abhishek Aravindan

Reputation: 1482

you can modify it in your building controller in create action.

def create
  @building = Building.new(building_params)
  if @building.save
    floors = params[:number].to_i
    floors.times do
      Floor.create(building: @building)
    end 
    redirect_to building_path
  else
    redirect_to error
  end
end

in your form add a field for number of floor without erb

<%= form_with(model: building, local: true) do |form| %>
  <% if building.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(building.errors.count, "error") %> prohibited this building 
       from being saved:</h2>

     <ul>
       <% building.errors.full_messages.each do |message| %>
        <li><%= message %></li>
        <% end %>
       </ul>
      </div>
      <% end %>

      <div class="field">
         <%= form.label :name %>
          <%= form.text_field :name %>
      </div>

      <div class="field">
        <label> How many floors does the building have</label>
        <input type="number" name="number"/>
      </div>

      <div class="actions">
        <%= form.submit %>
      </div>
   <% end %>

Upvotes: 1

GProst
GProst

Reputation: 10237

You can use nested attributes to help you here.

First, in your Building model add:

has_many :floors
accepts_nested_attributes_for :floors

Then in your BuildingController's create action do:

class BuildingController < ApplicationController
  ...
  def create
    floors_amount = params[:building][:floors] || 0
    building_params = params.require(:building).permit(:name).merge({
      # just fill floors array with empty hashes since `id` will be added automatically
      floors_attributes: (1..floors_amount).to_a.fill({})
    })
    @building = Building.create(building_params)
    if @buildnig.errors
      render :new # show the form page with an error
    else
      redirect_to @building # or whenever you want to redirect
    end
  end
  ...
end

The advantage here is that it wraps the creation of building and floors into a transaction so if something goes wrong it will rollback all changes. I.e. there won't be a case where new Building is inserted into DB but floors didn't due to some error during their creation.

Another convenience here is that if some validation error appears either on building or on any of the floor then it will be set into building.errors. Means you can easily do render :new in the case of errors to display them.

Note:

I see that in your form view you access Building instance as building. So I'm not really sure how you pass it to this view. In my example I saved building into @building variable so that in this view form you will have to access it as @building. I believe in your new action in BuildingController you should set it to @building as well:

class BuildingController < ApplicationController
  ...
  def new
    @building = Building.new
  end
  ...
end

And in your view you will access it as @building then, not building.

Hope, that makes sense

Upvotes: 0

Related Questions