Multiple records in one form

I would like to create a form that has this format:

Shared Fields
Fields Unique to record A
[add another record button]

      

If the [add] button can be clicked as many times as necessary to create

Shared Fields
Fields Unique to record A
Fields Unique to record B
Fields Unique to record C
[add another record button]

      

Submitting the form creates records A, B and C

back side

This creates 3 records, each with its own common and unique attributes according to the fields in the form.

Update

One thing that is shared is each one belonging to the "employer". So this should be form_for @employer?

Thinking out loud, we could keep the shared attributes as attr_accessors on the parent and assign them to the children in the child # create child controller action.

+1


source to share


3 answers


Since you are filling multiple models in one form without nesting, you cannot simply generate the form with form_for @something

, since you do not have a single object to fill out.

I would look at the database structure to see if there is a way to retrieve the common anto fields for a particular model. But if there is no way to do it cleanly, read on.

The Rails object form rendering helpers lay out fields in such a way that Rails can correctly parse as a hash. Which, in turn, can be used to construct an object. Using this:

form_for @thing do |f|
  t.text_field :name
end

      

You will get a form with a field thing[name]

that looks params

like this:

{..., thing: {name: "Hello world"}, ...}

      

See the guides for details .

And now here's the catch: you don't have one object to fill. But you can neglect this and build a form that will still be processed as a nested hash. You just have to manually fill in what Rails would otherwise assume: form the URL and object name.

form_for :thing, url: thing_path do |f|
  f.text_field :name
end

      

If you want to create a group of fields, use the fields_for

helper inside this form. More information in the documentation in a form that might look like this:

form_for :thing, url: thing_url do |f|
  f.text_field :name
  f.fields_for :something do |s|
    s.text_field :value
  end
end

      



This should render the field to be processed as a single hash. But you might want an array of nested fields in your case, so see the documentation.

You end up with:

  • Hash
    • Common parameters
    • Array of objects
    • Object A
    • Object A
    • Object B
    • ...

And now the final problem - you need to add fields to the form via JavaScript, but he needs to know in advance which label to add. There are many ways to solve this problem: one could submit a rough shape for each unique model and sample it using JavaScript.

Edit: Coincidentally, I needed to figure out how to render a "naked" (not object supported) form that can be copied by JavaScript and will look like an array of objects in parameters when multiple forms are given.

The tricky part: if you use fields_for :something

it will give a shape for a single object, not an array of them. Digging around, I discovered what appears to be an undocumented feature (digging through the code from this post ). It's used like this (I'm using the HAML / Slim syntax for brevity):

= form_for :thing, url: whatever do |f|
  = f.fields_for :stuff, index: '' do |s| #<< the `index: ''` part
    = s.text_field :v

      

Semantically, this means the following: create a form containing a field stuff

that is filled with one or many fields with an empty index . Under the hood, it generates a few awkward field names at first glance:

thing[stuff][v]   # Before, without ` index: '' `
thing[stuff][][v] # After, see the empty index?

      

The interesting part of this is that you can just clone the resultig value set without modifying it, and Rails (or even Rack?) Will resolve that form set into a separate object each. It depends on the browser preserving the field order, which is true in most cases.

+2


source


In your new.html.erb

<div id="formDiv">
  <%= form_for @user do |f| %>
  <%= render 'shared/error_messages' %><br>
    <%= f.label :name %><br>
    <%= f.text_field :name %><br>
    <%= f.label :email %><br>
    <%= f.email_field :email %><br>
    <%= f.label :password %><br>
    <%= f.password_field :password %><br>
    <%= f.label :password_confirmation, "Confirmation" %><br>
    <%= f.password_field :password_confirmation %><br>
    <%= f.submit "Create my account", class: "btn btn-primary" %>
  <% end %>
  <input id="add" type="submit" value="Add"/>
</div>
<script>
$("#add").click(function() {
  $.ajax({
    url: "newAjax",
    success: function (html) {
      $("#formDiv").append(html);
    }
  });
});
$('form').submit(function(){
  $('.multiform').each(function() {
    $(this).submit();
  });
  return true;
});
</script>

      

This uses AJAX to add a new form every time you click the button, and when you try to submit the form, it loops through other forms with the "multiform" class and submits them first.

In your newAjax.html.erb



<%= form_for @user, remote: true, html: { class: 'multiForm' }  do |f| %>
<%= f.label :name %><br>
  <%= f.text_field :name %><br>
  <%= f.label :email %><br>
  <%= f.email_field :email %><br>
  <%= f.label :password %><br>
  <%= f.password_field :password %><br>
  <%= f.label :password_confirmation, "Confirmation" %><br>
  <%= f.password_field :password_confirmation %><br>
<% end %>

      

We use AJAX on these forms so that they can be submitted without redirection. And the last one in your controller

def newAjax
  @user = User.new
  render :layout => false
end

      

We want to make sure our layout is not rendered with an AJAX form, so we don't reload JS / CSS files, etc.

0


source


Here is the working code I used at the end. Thanks to everyone who helped. The next method is D-side, but using "Employer" as the base model. Perhaps I could refactor this to remove attr_accessors in Employer and use field tags in the view instead. At the end of the day, I am only pulling the shared values ​​from the "params" array passed to the controller.

Order controller:

 def new
    @employer = current_employer
    @booking = @employer.bookings.build
    respond_with(@booking)
  end

  def create
    errors = []
    booking_create_params["new_booking_attributes"].each do |booking|
      new_booking = current_employer.bookings.build(booking)
      new_booking.job_type_id = booking_create_params["job_type_id"]
      new_booking.vehicle_id = booking_create_params["vehicle_id"]
      new_booking.location_id = booking_create_params["location_id"]
      new_booking.pay = booking_create_params["pay"]
      unless new_booking.save
        errors << new_booking.errors.full_messages
      end
    end

    if errors == []
      flash[:notice] = "Bookings posted!"
      redirect_to new_booking_path
    else
      flash[:notice] = "Error: #{errors.join(', ')}!"
      redirect_to new_booking_path
    end
  end

      

In view:

  <div id="bookings">
      <ol class="numbers">
        <li>
          <legend>Location, Pay, & Vehicle</legend>

          <div class="form-group">
            <div class="row">
              <div class="col-sm-6">
                <label>Type of job</label><br>
                <%= f.select(:job_type_id, options_from_collection_for_select(JobType.all, :id, :name_with_delivery), {}, { id: 'job-type', class: 'form-control' }) %>
              </div>
              <div class="col-sm-6">
                <label>Vehicle needed</label><br>
                <%= f.select(:vehicle_id, options_from_collection_for_select(Vehicle.all, :id, :name), {}, { id: 'vehicle-type', class: 'form-control' }) %>
              </div>
            </div>
          </div>

          <div class="form-group">
            <div class="row">
              <div class="col-sm-6">
                <label>Location</label>
                <% if current_employer.locations.count > 1 %>
                  <%= f.select :location_id, options_from_collection_for_select(current_employer.locations.all, :id, :name_or_address_1), {}, { class: 'form-control' } %>
                <% elsif current_employer.locations.count == 1 %>
                  <p><strong>Location: </strong><%= current_employer.locations.first.name_or_address_1 %></p>
                  <%= f.hidden_field :location_id, current_employer.locations.first.id %>
                <% end %>
                <%= link_to "or add new location", new_employer_location_path(current_employer, Location.new) %>
              </div>
              <div class="col-sm-6">
                <%= f.label :pay %><br>
                <%= f.text_field :pay, class: 'form-control' %>
              </div>
            </div>
          </div>

        </li>
        <legend>Shifts</legend>
        <%= render 'booking', booking: Booking.new %>
      </ol>
  </div>

      

If partial:

<li class="close-list-item">

<!--   Recommended: post at least 1 week in advance 
  & shift length at least 4 hours.
   -->
  <% new_or_existing = booking.new_record? ? 'new' : 'existing' %>
  <% prefix = "employer[#{new_or_existing}_booking_attributes][]" %>
  <%= fields_for prefix, booking do |booking_form| -%>

  <div class="shifts">
    <div class="form-group shift">
      <div class="row">
        <div class="col-sm-4">
          <label for="">Date</label> 
          <i class="glyphicon glyphicon-time"></i>
          <%= booking_form.text_field :start, class: 'form-control booking-date', placeholder: 'Date', data: { provide: "datepicker", date_clear_btn: "true", date_autoclose: "true", date_start_date: '+1d', date_format: "yyyy-mm-dd" } %>                  
        </div>
        <div class="col-sm-4">
          <label>Time Start</label><br>
          <%= booking_form.select(:start_time, options_for_select(
            booking_times_array
          ), { include_blank: true }, { class: 'form-control booking-time booking-time-start' } ) %>
        </div>
        <div class="col-sm-4">
          <label>
            Time End
          </label>
          <%= link_to "javascript:;", class: 'pull-right remove-shift' do %>
              <i class="glyphicon glyphicon-remove"></i>
            <% end %>
            <script type="text/javascript">
              $(".remove-shift").click(function(){
                $(this).parents("li").remove();
              });
            </script>
          <%= booking_form.select(:end_time, options_for_select(
          booking_times_array
        ), { include_blank: true }, { class: 'form-control booking-time booking-time-end' } ) %>
        </div>
      </div>
    </div>
  </div>
</li>
<% end -%>

      

0


source







All Articles