1

I'm having trouble grasping this issue. I have a model (Invoice) that belongs_to another model (Customer) and a customer has_many invoices.

So an invoice cannot be created without a customer. I would like to be able to create a customer with the invoice at the same time with the same form.

Right now I just have it so you can manually punch in an invoice_id and it all works fine but that isn't ideal for someone actually using the application for obvious reasons.

I've read up on this article but I still don't understand and was also unsure if this is still applicable with Rails 5: https://stackoverflow.com/a/3809360/7467341

Is the above answer still the correct approach? Could someone clarify and maybe give me some code to try? Thanks!

<%= form_for(invoice) do |f| %>
    <% if invoice.errors.any? %>
        <div id="error_explanation">
            <h2><%= pluralize(invoice.errors.count, "error") %>
                prohibited this invoice from being saved:</h2>

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


    <h2>Customer Info</h2>
    <hr>
    <div class="field">
        <%= f.label :customer_id %>
        <%= f.number_field :customer_id %>
    </div>

    <!-- My customer model has more fields that I want to go here (name, address, and phone_number) -->

    [...] other regular invoice fields here...

    <div class="actions">
        <%= f.submit %>
    </div>
<% end %>
Taylor A. Leach
  • 1,377
  • 1
  • 17
  • 34

4 Answers4

1

You may want to use: cocoon to create dynamic nested forms

eugene
  • 530
  • 4
  • 15
1

There are quite a few different libraries that can help out for situations like this. But I'll try to suggest a solution that could work with Rails only.

So trying to analyse your situation, I'm guessing that we need to tackle two scenarios. The first for adding an Invoice for an existing Customer, and the second for creating a new Customer at the same time as creating the Invoice. The most straight forward way of doing this, in my opinion, would be to add two separate fields in your form. The first field being a Select dropdown where you can choose from existing Customers, and the second one a Textfield where you input the name of the Customer if there is no match in the Select field.

The logic would then be to first check if anything was selected in the dropdown, in which case any text in the textfield would be ignored. But if nothing was selected from the dropdown, then it would create the Customer based on the Textfield.

Here is how I suggest to achieve this:

class Invoice < ActiveRecord::Base

  belongs_to :customer

  validate :customer_id, presence: true

  # A dynamic attribute that is NOT represented by a database column in the Invoice model, but only used for the form
  attr_accessor :customer_name, :customer_address #, plus your other fields

  before_validation on: :create do
    if customer_id.nil? && customer_name.present?
      # Use create! (with !) so that an error is raised if the customer 
      # could not be created which will abort the entire save transaction
      self.customer = Customer.create!(name: customer_name, address: customer_address)
      # Add your other customer attributes above as well
    end
  end

end

And then in your form

<%= form_for(invoice) do |f| %>

    <h2>Customer Info</h2>
    <hr>
    <div class="field">
        <%= f.label :customer_id, "Choose existing Customer" %>
        <%= f.select :customer_id, options_from_collection_for_select(Customer.all, :id, :name, invoice.customer_id) %>
    </div>

    <div class="field">
        <%= f.label :customer_name, "... or create a new Customer" %>
        <%= f.text_field :customer_name %>
    </div>

    <div class="field">
        <%= f.label :customer_address, "New customer address" %>
        <%= f.text_field :customer_address %>
    </div>
    <!-- Repeat for all your other Customer attributes -->

    [...] other regular invoice fields here...

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

And don't forget the permit the additional params attributes from the Controller Invoice.create(params.permit(:customer_name, :customer_address, ...)).

There are certainly a few things that can be made more efficient in this, but I hope this can showcase some of the features that Rails offer out-of-the-box to tackle scenarios like this, and I think it is good to focus on that if you are somewhat new to Rails.

DanneManne
  • 20,521
  • 5
  • 53
  • 54
  • This makes the most sense and is what I was looking for. Thank you. I will have to address the selecting of old customers because it will obviously be insane if there is any large amount of data. I had to tweak your code to add a blank space in the dropdown or else it would always auto select an old customer and never save a new on with `:include_blank => true` at the end of the `options_from_collection_for_select` – Taylor A. Leach Feb 01 '18 at 15:46
  • Right, I should have thought about `:include_blank`, but I forgot. In any case, glad you got it working. – DanneManne Feb 02 '18 at 00:44
  • I appreciate the explanation. Much more helpful than linking to an article or some gem. Now that I understand how to approach the problem, I'm much more confident going forward and can confidently start looking into one of the gems that helps out with an issue like this. Thanks. – Taylor A. Leach Feb 02 '18 at 00:47
1

You can use nested attributes in this case. In customer.rb file, add this accepts_nested_attributes_for :invoices.

In whitelisting parameters, add invoices_attribues: [invoice_fields] along with customer fields.

Check this link for understanding fields_for and nested attributes. Check this link to learn about how nested attributes work.

0

That's a nested form you try to do there:

  1. Rails Guide, 9.2 Nested Forms
  2. simple_form, cocoon
  3. nested forms on google
  4. Nested Forms Rails
razvans
  • 1,190
  • 3
  • 15
  • 18