Rails Form Object with Virtus: has_many association

前端 未结 3 888
萌比男神i
萌比男神i 2020-12-17 06:24

I am having a tough time figuring out how to make a form_object that creates multiple associated objects for a has_many association with the virtus gem.

相关标签:
3条回答
  • 2020-12-17 06:44

    You have an issue because you haven't whitelisted any attributes under :emails. This is confusing, but this wonderful tip from Pat Shaughnessy should help set you straight.

    This is what you're looking for, though:

    params.require(:user_form).permit(:name, { emails: [:email_text, :id] })
    

    Note the id attribute: it's important for updating the records. You'll need to be sure you account for that case in your form objects.

    If all this form object malarkey with Virtus gets to be too much, consider Reform. It has a similar approach, but its raison d'etre is decoupling forms from models.


    You also have an issue with your form… I'm not sure what you were hoping to achieve with the syntax you're using, but if you look at your HTML you'll see that your input names aren't going to pan out. Try something more traditional instead:

    <%= f.fields_for :emails do |ff| %>
      <%= ff.text_field :email_text %>
    <% end %>
    

    With this you'll get names like user_form[emails][][email_text], which Rails will conveniently slice and dice into something like this:

    user_form: { 
      emails: [
        { email_text: '...', id: '...' },
        { ... }
      ]
    }
    

    Which you can whitelist with the above solution.

    0 讨论(0)
  • 2020-12-17 06:57

    The problem is that the format of the JSON being passed to UserForm.new() is not what is expected.

    The JSON that you are passing to it, in the user_form_params variable, currently has this format:

    {  
       "name":"testform",
       "emails":{  
          "0":{  
             "email_text":"email1@test.com"
          },
          "1":{  
             "email_text":"email2@test.com"
          },
          "2":{  
             "email_text":"email3@test.com"
          }
       }
    }
    

    UserForm.new() is actually expecting the data in this format:

    {  
       "name":"testform",
       "emails":[   
           {"email_text":"email1@test.com"}, 
           {"email_text":"email2@test.com"},  
           {"email_text":"email3@test.com"}
       }
    }
    

    You need to change the format of the JSON, before passing it to UserForm.new(). If you change your create method to the following, you won't see that error anymore.

      def create
        emails = []
        user_form_params[:emails].each_with_index do |email, i| 
          emails.push({"email_text": email[1][:email_text]})
        end
    
        @user_form = UserForm.new(name: user_form_params[:name], emails: emails)
    
        if @user_form.save
          redirect_to @user, notice: 'User was successfully created.' 
        else
          render :new 
        end
      end
    
    0 讨论(0)
  • 2020-12-17 07:00

    I would just set the emails_attributes from user_form_params in the user_form.rb as a setter method. That way you don't have to customize the form fields.

    Complete Answer:

    Models:

    #app/modeles/user.rb
    class User < ApplicationRecord
      has_many :user_emails
    end
    
    #app/modeles/user_email.rb
    class UserEmail < ApplicationRecord
      # contains the attribute: #email
      belongs_to :user
    end
    

    Form Objects:

    # app/forms/user_form.rb
    class UserForm
      include ActiveModel::Model
      include Virtus.model
    
      attribute :name, String
    
      validates :name, presence: true
      validate  :all_emails_valid
    
      attr_accessor :emails
    
      def emails_attributes=(attributes)
        @emails ||= []
        attributes.each do |_int, email_params|
          email = EmailForm.new(email_params)
          @emails.push(email)
        end
      end
    
      def save
        if valid?
          persist!
          true
        else
          false
        end
      end
    
    
      private
    
      def persist!
        user = User.new(name: name)
        new_emails = emails.map do |email|
          UserEmail.new(email: email.email_text)
        end
        user.user_emails = new_emails
        user.save!
      end
    
      def all_emails_valid
        emails.each do |email_form|
          errors.add(:base, "Email Must Be Present") unless email_form.valid?
        end
        throw(:abort) if errors.any?
      end
    end 
    
    
    # app/forms/email_form.rb
    # "Embedded Value" Form Object.  Utilized within the user_form object.
    class EmailForm
      include ActiveModel::Model
      include Virtus.model
    
      attribute :email_text, String
    
      validates :email_text,  presence: true
    end
    

    Controller:

    # app/users_controller.rb
    class UsersController < ApplicationController
    
      def index
        @users = User.all
      end
    
      def new
        @user_form = UserForm.new
        @user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
      end
    
      def create
        @user_form = UserForm.new(user_form_params)
        if @user_form.save
          redirect_to users_path, notice: 'User was successfully created.'
        else
          render :new
        end
      end
    
      private
        def user_form_params
          params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
        end
    end
    

    Views:

    #app/views/users/new.html.erb
    <h1>New User</h1>
    <%= render 'form', user_form: @user_form %>
    
    
    #app/views/users/_form.html.erb
    <%= form_for(user_form, url: users_path) do |f| %>
    
      <% if user_form.errors.any? %>
        <div id="error_explanation">
          <h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
    
          <ul>
          <% user_form.errors.full_messages.each do |message| %>
            <li><%= message %></li>
          <% end %>
          </ul>
        </div>
      <% end %>
    
      <div class="field">
        <%= f.label :name %>
        <%= f.text_field :name %>
      </div>
    
    
      <%= f.fields_for :emails do |email_form| %>
        <div class="field">
          <%= email_form.label :email_text %>
          <%= email_form.text_field :email_text %>
        </div>
      <% end %>
    
    
      <div class="actions">
        <%= f.submit %>
      </div>
    <% end %>
    
    0 讨论(0)
提交回复
热议问题