简体   繁体   English

Rails表单对象与Virtus:has_many关联

[英]Rails Form Object with Virtus: has_many association

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 . 我很难搞清楚如何创建一个form_object,为与virtus gemhas_many关联创建多个关联对象。

Below is a contrived example where a form object might be overkill, but it does show the issue I am having: 下面是一个人为的例子,表单对象可能有点过分,但确实显示了我遇到的问题:

Lets say there is a user_form object that creates a user record, and then a couple associated user_email records. 假设有一个user_form对象可以创建user记录,然后是一对关联的user_email记录。 Here are the models: 以下是模型:

# models/user.rb
class User < ApplicationRecord
  has_many :user_emails
end

# models/user_email.rb
class UserEmail < ApplicationRecord
  belongs_to :user
end

I proceed to create aa form object to represent the user form: 我继续创建一个表单对象来表示用户表单:

# app/forms/user_form.rb
class UserForm
  include ActiveModel::Model
  include Virtus.model

  attribute :name, String
  attribute :emails, Array[EmailForm]

  validates :name, presence: true

  def save
    if valid?
      persist!
      true
    else
      false
    end
  end

  private

  def persist!
    puts "The Form is VALID!"
    puts "I would proceed to create all the necessary objects by hand"

    # user = User.create(name: name)
    # emails.each do |email_form|
    #   UserEmail.create(user: user, email: email_form.email_text)
    # end
  end
end

One will notice in the UserForm class that I have the attribute :emails, Array[EmailForm] . UserForm类中我会注意到我有以下attribute :emails, Array[EmailForm] This is an attempt to validate and capture the data that will be persisted for the associated user_email records. 这是尝试验证和捕获将为相关user_email记录user_email数据。 Here is the Embedded Value form for a user_email record: 以下是user_email记录的Embedded Value表单:

# app/forms/email_form.rb
# Note: this form is an "Embedded Value" Form Utilized in user_form.rb
class EmailForm
  include ActiveModel::Model
  include Virtus.model

  attribute :email_text, String

  validates :email_text,  presence: true
end

Now I will go ahead and show the users_controller which sets up the user_form. 现在,我会继续前进,显示users_controller其中规定了user_form。

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  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 @user, notice: 'User was successfully created.' 
    else
      render :new 
    end
  end

  private
    def user_form_params
      params.require(:user_form).permit(:name, {emails: [:email_text]})
    end
end

The new.html.erb : new.html.erb

<h1>New User</h1>

<%= render 'form', user_form: @user_form %>

And the _form.html.erb : _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>

  <% unique_index = 0 %>
  <% f.object.emails.each do |email| %>
    <%= label_tag       "user_form[emails][#{unique_index}][email_text]","Email" %>
    <%= text_field_tag  "user_form[emails][#{unique_index}][email_text]" %>
    <% unique_index += 1 %>
  <% end %>

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

Note: If there is an easier, more conventional way to display the inputs for the user_emails in this form object: let me know. 注意:如果有一种更简单,更传统的方式来显示此表单对象中user_emails的输入:请告诉我。 I could not get fields_for to work. 我无法让fields_for工作。 As shown above: I had to write out the name attributes by hand. 如上所示:我必须手工写出name属性。

The good news is that the form does render: 好消息是表单确实呈现:

渲染形式

The html of the form looks ok to me: 表单的html对我来说没问题:

表格的HTML

When the above input is submitted: Here is the params hash: 提交上述输入时:这是params哈希:

Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "emails"=>{"0"=>{"email_text"=>"foofoo"}, "1"=>{"email_text"=>"bazzbazz"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}

The params hash looks ok to me. params hash对我来说很好看。

In the logs I get two deprecation warnings which makes me think that virtus might be outdated and thus no longer a working solution for form objects in rails: 在日志中我得到两个弃用警告,这让我觉得virtus可能已经过时,因此不再是rails中表单对象的工作解决方案:

DEPRECATION WARNING: Method to_hash is deprecated and will be removed in Rails 5.1, as ActionController::Parameters no longer inherits from hash. 弃用警告:方法to_hash已弃用,将在Rails 5.1中删除,因为ActionController::Parameters不再继承自hash。 Using this deprecated behavior exposes potential security problems. 使用此弃用行为会暴露潜在的安全问题。 If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. 如果您继续使用此方法,则可能会在您的应用中创建可被利用的安全漏洞。 Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1) DEPRECATION WARNING: Method to_a is deprecated and will be removed in Rails 5.1, as ActionController::Parameters no longer inherits from hash. 相反,请考虑使用其中一种未被弃用的文档化方法: http//api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (从new at(pry)调用:1)DEPRECATION警告:方法不推荐使用to_a,它将在Rails 5.1中删除,因为ActionController::Parameters不再继承自hash。 Using this deprecated behavior exposes potential security problems. 使用此弃用行为会暴露潜在的安全问题。 If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. 如果您继续使用此方法,则可能会在您的应用中创建可被利用的安全漏洞。 Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1) NoMethodError: Expected ["0", "foofoo"} permitted: true>] to respond to #to_hash from /Users/neillocal/.rvm/gems/ruby-2.3.1/gems/virtus-1.0.5/lib/virtus/attribute_set.rb:196:in `coerce' 相反,请考虑使用其中一个未弃用的文档化方法: http ://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html(从new at(pry)调用:1)NoMethodError:Expected [允许:“0”,“foofoo”}允许:true>]从/Users/neillocal/.rvm/gems/ruby-2.3.1/gems/virtus-1.0.5/lib/virtus/attribute_set.rb回复#to_hash :196:在'coerce'

And then the whole thing errors out with the following message: 然后整个事情错误地出现以下消息:

Expected ["0", <ActionController::Parameters {"email_text"=>"foofoo"} permitted: true>] to respond to #to_hash

I feel like I am either close and am missing something small in order for it to work, or I am realizing that virtus is outdated and no longer usable (via the deprecation warnings). 我觉得我要么接近并且为了工作而丢失了一些小东西,要么我意识到virtus已经过时且不再可用(通过弃用警告)。

Resources I looked at: 我看过的资源:

I did attempt to get the same form to work but with the reform-rails gem . 我确实试图让同样的形式工作,但改革轨道宝石 I ran into an issue there too. 我也遇到了一个问题。 That question is posted here . 这个问题发布在这里

Thanks in advance! 提前致谢!

I would just set the emails_attributes from user_form_params in the user_form.rb as a setter method. 我只是将user_form.rb中user_form_params的emails_attributes设置为setter方法。 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 %>

You have an issue because you haven't whitelisted any attributes under :emails . 您遇到问题,因为您未将以下任何属性列入白名单:emails This is confusing, but this wonderful tip from Pat Shaughnessy should help set you straight . 这很令人困惑,但是来自Pat Shaughnessy的精彩提示应该会帮助你顺利

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. 注意id属性:它对于更新记录很重要。 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 . 如果所有这些形式对象与Virtus的malarkey太多,请考虑改革 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. 您的表单也存在问题...我不确定您希望使用您正在使用的语法实现什么,但是如果您查看HTML,您会看到您的输入名称不会出现问题。 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]这样的user_form[emails][][email_text] ,Rails会方便地切片和切成这样的东西:

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

Which you can whitelist with the above solution. 您可以使用上述解决方案将其列入白名单。

The problem is that the format of the JSON being passed to UserForm.new() is not what is expected. 问题是传递给UserForm.new()的JSON格式不是预期的。

The JSON that you are passing to it, in the user_form_params variable, currently has this format: 您在user_form_params变量中传递给它的JSON当前具有以下格式:

{  
   "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: UserForm.new()实际上是期望这种格式的数据:

{  
   "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() . 在将JSON传递给UserForm.new()之前,您需要更改JSON的格式。 If you change your create method to the following, you won't see that error anymore. 如果将create方法更改为以下内容,则不会再看到该错误。

  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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM