2017-03-17 154 views
3

我有一个艰难的时间搞清楚如何使一个form_object创建多个关联对象has_manyvirtus gem关联。Rails窗体对象与Virtus:has_many关联

下面是一个人为的例子,其中一个表单对象可能是矫枉过正,但它确实表明我遇到的问题:

比方说,有一个创建user记录一user_form对象,然后一对夫妇的相关user_email记录。下面是型号:

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

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

我继续创建一个表单对象来表示用户形式:

# 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将在UserForm类,我有attribute :emails, Array[EmailForm]通知。这是试图验证和捕获将为相关的user_email记录保留的数据。这里是Embedded Value形式为user_email记录:

# 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 

现在,我会继续前进,显示其中规定了user_form的users_controller

# 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 

new.html.erb

<h1>New User</h1> 

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

而且_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 %> 

注:如果有一个更简单,更传统的方式显示在此user_emails输入形式对象:让我知道。我无法获得fields_for的工作。如上所示:我必须亲手写出name属性。

的好消息是,形式也呈现:

rendered form

形式的HTML看起来不错对我说:

html of the form

当上述输入提交:在这里是参数哈希:

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"} 

params哈希对我来说看起来不错。

在日志中我得到两个废弃警告,这让我觉得VIRTUS可能已经过期,并在轨道表单对象从而不再是一个有效的解决方案:

DEPRECATION WARNING: Method to_hash is deprecated and will be removed in Rails 5.1, as ActionController::Parameters no longer inherits from 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. 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'

然后整个事情出错了与以下消息:

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

我觉得我要么关闭,我失去了一些东西小,以便为它工作,或者我意识到VIRTUS已经过时,不再可用(通过废弃警告)。

资源我看了看:

我没有试图让相同的形式工作,但与reform-rails gem。我也遇到了一个问题。那个问题是posted here

在此先感谢!

回答

2

我只是将user_form.rb中user_form_params的emails_attributes设置为setter方法。这样你就不必自定义表单域。

完整的答案:

型号:

#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 

表单对象:

# 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 

控制器:

# 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 

浏览:

#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

请接受修改,我会很乐意将您的答案标记为已接受的答案。稍作修改以使验证按预期工作,然后为了方便:为所有主要部分提供代码。我想给你信贷,因为你张贴的答案让我通过了我被卡住的部分。 – Neil

+0

应该指出的是,人们应该考虑使用virtu gem为你的表单对象带来的好处。机会是:你可以很容易地创建你的表单对象,而不需要将virt-gem依赖项添加到你的项目中。 [见Rails窗体对象实现在这里](http://stackoverflow.com/questions/42930117/form-objects-in-rails/43024542#43024542) – Neil

+0

感谢您的答案。这正是我一直在寻找的。 虽然我是Rails newb。有人可以向我解释为什么我们需要定义'emails_attributes =(属性)'。这是什么调用?我发现如果这个方法没有定义,那么'@user_form.emails = [EmailForm.new,EmailForm.new,EmailForm.new]'不能按预期工作。 Rails中的这个魔术发生在哪里? – user3574603

0

您遇到问题是因为您尚未列入:emails下的任何属性。这很让人困惑,但是this wonderful tip from Pat Shaughnessy should help set you straight

这是你在找什么,但:

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

注意id属性:它是用于更新记录很重要。你需要确保你在表单对象中处理这种情况。

如果所有与Virtus组成的对象malarkey变得太多,请考虑Reform。它有类似的方法,但其存在的理由是将模型与模型脱钩。


你也有一个问题,你的形式......我不知道你希望实现与你正在使用的语法是什么,但如果你看看你的HTML,你会看到你的名字输入不会平息。尝试更多的东西,而不是传统:

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

有了这个,你会得到象user_form[emails][][email_text],这Rails会方便大卸八块弄成这个样子:

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

,您可以用上述溶液白名单。

+0

谢谢你的留言。不幸的是,电子邮件仍然没有传递到params散列:''user_form“=> {”name“=>”neil“,”emails“=>”“}'。它仍然这样说:'未经许可的参数:电子邮件' – Neil

+0

我没有意识到你没有得到'emails'下的任何值。它不允许空字符串,因为它现在期待一个散列。查看更新的答案。 – coreyward

0

的问题是,被传递到UserForm.new()了JSON格式不被期望的。

要传递给它,在user_form_params变量的JSON,目前有以下格式:

{ 
    "name":"testform", 
    "emails":{ 
     "0":{ 
     "email_text":"[email protected]" 
     }, 
     "1":{ 
     "email_text":"[email protected]" 
     }, 
     "2":{ 
     "email_text":"[email protected]" 
     } 
    } 
} 

UserForm.new()实际上是预期中的数据格式为:

{ 
    "name":"testform", 
    "emails":[ 
     {"email_text":"[email protected]"}, 
     {"email_text":"[email protected]"}, 
     {"email_text":"[email protected]"} 
    } 
} 

您需要在将其传递到UserForm.new()之前,更改JSON的格式。如果您将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