2017-03-17 59 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 
相關問題