0

我已经围绕这个圈了几个圈子。

Regions我在更新时遇到问题Listing

现在我什至无法在创建时Regions添加多个。Listing如果有人可以帮助我解决问题,那就太好了。从新鲜(有经验的)眼睛看我的代码可能会注意到我在做什么是愚蠢的。

  • 两种型号:ListingRegion
  • 第三种加盟模式:Regionalization

楷模:

# app/models/listing.rb
class Listing < ApplicationRecord
  has_many :regionalizations
  has_many :regions, through: :regionalizations
  
  accepts_nested_attributes_for :regionalizations, allow_destroy: true, reject_if: :all_blank
end

# app/models/region.rb
class Region < ApplicationRecord
  has_many :regionalizations
  has_many :listings, through: :regionalizations  
end

# app/models/regionalization.rb
class Regionalization < ApplicationRecord
  belongs_to :listing
  belongs_to :region
end

模型和关联对我来说似乎是合理的。我认为问题在于控制器和/或嵌套形式。

控制器操作 [注意我正在为此控制器使用仪表板命名空间]

class Dashboard::ListingsController < Dashboard::BaseController      
  def new
    @listing = Listing.new
  end
    
  def create
    @listing = Listing.new(listing_params)
    @listing.user_id = current_user.id
        
    @listing.regionalizations.build
    
    if @listing.save
      redirect_to dashboard_path, notice: "Your Listing was created successfuly"
    else
      render :new
    end  
  end
    
  def update
    respond_to do |format|
      if @listing.update(listing_params)  
        format.html { redirect_to edit_dashboard_listing_path(@listing), notice: 'Your Listing was successfully updated.' }
        format.json { render :show, status: :ok, location: @listing }
      else
        format.html { render :edit }
        format.json { render json: @listing.errors, status: :unprocessable_entity }
      end
    end
  end  
    
  private
    
  def listing_params
    params.require(:listing).permit(:id, :name, :excerpt, :description, :email, :website, :phone_number, :user_id, :featured_image, :business_logo, :address, :category_id, :facebook, :instagram, :twitter, :status, :regionalization_id, gallery_images: [], regionalizations_attributes: [:id, :region_id, :listing_id, :_destroy])
  end
end

仪表板/列表/_form:

<%= form_with(model: [:dashboard, listing], local: true) do |f| %>    
  <article class="card mb-3">
    <div class="card-body">                         
      <h5 class="card-title mb-4">Delivery Regions</h5>                                                     
      <%= f.fields_for :regionalizations do |regionalizations_form| %>
        <%= render 'regionalization_fields', f: regionalizations_form %>
      <% end %>
      <%= link_to_add_fields "Add Region", f, :regionalizations %>                      
    </div>
  </article>
  <%= f.submit data: { turbolinks: false }, class: "btn btn-outline-primary" %>
<% end %>

_regionalization_fields.html.erb:

<p class="nested-fields">
  <%= f.collection_select(:region_id, Region.all, :id, :name, {multiple: true}, {class: 'form-control'}) %>
  <%= f.hidden_field :_destroy %>
  <%= link_to "Remove", '#', class: "remove_fields" %>
</p>

创建新时验证错误Listing

Regionalizations region must exist

如果我将它添加到区域化表中,我可以让区域化工作。

belongs_to :region, optional: true

现在我的参数只显示一个区域化属性,除非我告诉它构建 3 或 4。

像这样:

4.times do @listing.regionalizations.build end

我已经使用Steve Polito 的指南来尝试让它工作。我没有更改任何 javascript 内容或 application_helper 内容。

添加和删​​除字段在前端工作正常。删除嵌套字段在 dB 中工作正常。

请问我在这里错过了什么完全愚蠢的东西吗?

我唯一能注意到新嵌套字段和从构建方法中引入的任何不同之处是“选定”标签不在添加到表单的新嵌套字段上。

图片

提交参数:

Started POST "/dashboard/listings" for ::1 at 2020-10-30 20:22:15 +0000
Processing by Dashboard::ListingsController#create as HTML
  Parameters: {"authenticity_token"=>"shtfCS/cSj/w/I6S1tNey99L8TKf48Xj0GAOMsODU3l44o0pJdjucCteQXca496aosNCEp7sPD85UM4QO4jEnw==", "listing"=>{"name"=>"", "excerpt"=>"", "description"=>"", "category_id"=>"1", "email"=>"", "phone_number"=>"", "website"=>"", "address"=>"", "facebook"=>"#", "instagram"=>"#", "twitter"=>"#", "regionalizations_attributes"=>{"0"=>{"region_id"=>"14", "_destroy"=>"false"}}}, "commit"=>"Create Listing"}

我将添加从 Steve 的嵌套表单教程中获取的 applicaton_helper 文件。其中一条评论提到了代码的动态能力。它有效(只是不适合我)。我可以通过强制编号循环来实现我对 create 方法的需要。只是无法让字段动态添加到数据库中。

# This method creates a link with `data-id` `data-fields` attributes. These attributes are used to create new instances of the nested fields through Javascript.
  def link_to_add_fields(name, f, association)

      # Takes an object (@person) and creates a new instance of its associated model (:addresses)
      # To better understand, run the following in your terminal:
      # rails c --sandbox
      # @person = Person.new
      # new_object = @person.send(:addresses).klass.new
      new_object = f.object.send(association).klass.new

      # Saves the unique ID of the object into a variable.
      # This is needed to ensure the key of the associated array is unique. This is makes parsing the content in the `data-fields` attribute easier through Javascript.
      # We could use another method to achive this.
      id = new_object.object_id

      # https://api.rubyonrails.org/ fields_for(record_name, record_object = nil, fields_options = {}, &block)
      # record_name = :addresses
      # record_object = new_object
      # fields_options = { child_index: id }
          # child_index` is used to ensure the key of the associated array is unique, and that it matched the value in the `data-id` attribute.
          # `person[addresses_attributes][child_index_value][_destroy]`
      fields = f.fields_for(association, new_object, child_index: id) do |builder|

          # `association.to_s.singularize + "_fields"` ends up evaluating to `address_fields`
          # The render function will then look for `views/people/_address_fields.html.erb`
          # The render function also needs to be passed the value of 'builder', because `views/people/_address_fields.html.erb` needs this to render the form tags.
          render(association.to_s.singularize + "_fields", f: builder)
      end

      # This renders a simple link, but passes information into `data` attributes.
          # This info can be named anything we want, but in this case we chose `data-id:` and `data-fields:`.
      # The `id:` is from `new_object.object_id`.
      # The `fields:` are rendered from the `fields` blocks.
          # We use `gsub("\n", "")` to remove anywhite space from the rendered partial.
      # The `id:` value needs to match the value used in `child_index: id`.
      link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})

  end
4

1 回答 1

0

你的问题

您的错误是告诉您至少有一条Regionalization记录无法保存,因为它没有region_id.

添加belongs_to :region, optional: true实际上会破坏您的数据完整性。您现在有了存在Regionalization的记录Listing,但没有Region,这违背了Regionalization连接表的目的。另外,您可以在连接表中使用单面记录来完全膨胀您的数据库。

轻松路线

您需要拒绝Regionalization没有a又没有 a 的 s:listing_id:region_id

这个:

accepts_nested_attributes_for :regionalizations, allow_destroy: true, reject_if: :all_blank

不是为你做这项工作。记录不是“全空白”,因为它有一个:listing_id

在模型中处理这个是可行的:

# app/models/listing.rb
class Listing < ApplicationRecord
  has_many :regionalizations
  has_many :regions, through: :regionalizations
  
  accepts_nested_attributes_for :regionalizations, allow_destroy: true, reject_if: :missing_keys

  private

  def missing_keys(attributes)
    attributes['region_id'].blank? ||
    attributes['listing_id'].blank?
  end
end

v2 更新

因此,无论出于何种原因,regionalizations.build 只会通过参数发送第一个。如果我像问题中那样让它成为一个循环,它将把它们全部通过。

是的,@listing.regionalizations.build仅创建 1 条新记录。

如果您确切知道要在页面上显示多少个下拉菜单,则可以使用您的4.times do @listing.regionalizations.build end代码。这会将 4 条新记录加载到内存中,Rails 会在运行时将它们作为集合找到:

<%= render 'regionalization_fields', f: regionalizations_form %>

f.fields_for它不是一个循环,所以它的工作原理对我来说有点神秘。

如果要创建 4 个子记录,则应使用f.fields_for4 次。

你可以通过改变你的局部来做到这一点:

class Dashboard::ListingsController < Dashboard::BaseController      
  def new
    @listing = Listing.new
    4.times { @listing.regionalization.build }
    @regionalizations = @listing.regionalizations
  end
  ...
  def edit
    @listing = Listing.find(params[:id])
    4.times { @listing.regionalization.build }

    # will include ALL existing regionalizations, and 4 new ones
    @regionalizations = @listing.regionalizations
  end

仪表板/列表/_form:

<%= form_with(model: [:dashboard, listing], local: true) do |f| %>    
  <article class="card mb-3">
    <div class="card-body">                         
      <h5 class="card-title mb-4">Delivery Regions</h5>

      <%= render 'regionalization_fields', collection: @regionalizations %> 
                                             
      <%= link_to_add_fields "Add Region", f, :regionalizations %>                      
    </div>
  </article>
  <%= f.submit data: { turbolinks: false }, class: "btn btn-outline-primary" %>
<% end %>

重命名: _regionalization_fields.html.erb为了_regionalizations_form.html.erb清楚起见:

<!-- change the form handler's name so it doesn't conflict with the local variable 'regionalizations_form` -->
<%= f.fields_for regionalizations_form do |f_reg| %>
  <p class="nested-fields">
    <%= f_reg.collection_select(:region_id, Region.all, :id, :name, {multiple: true}, {class: 'form-control'}) %>
    <%= f_reg.hidden_field :_destroy %>
    <%= link_to "Remove", '#', class: "remove_fields" %>
  </p>
<% end %>

如果您需要可变数量的Regionalizations,最好的处理方法是使用一些 Rails AJAX 和 UJS。这意味着创建只有 1 条新Regionalization记录的表单并有一个显示“添加另一个”的按钮。用户单击它,然后您在另一个选择字段中添加您需要的所有内容,包括将结果发布到参数的正确约定。这是一个完全不同的蠕虫罐头,但我仍然推荐(史蒂夫波利托也是如此)Ryan Bate 的 Railscast 嵌套形式

更好的路线(仍然)

您选择了“has_many / belongs_to :through”选项,但如果Regionalization真的只是一个连接表,您可以使用.has_and_belongs_to_many

如果您不确定是否可以走这条路,请考虑一下:如果Regionalization不需要任何方法或除外键 ID 之外的其他 dB 字段,那么您已经使用不需要的模型增加了复杂性。

如果Regionalization可以是更标准的 JoinTable,则可以避免一些嵌套复杂性,并允许 Rails 为您完成。

如果您可以这样做,请参阅此文档。您需要更改迁移,但可以使用create_join_table让 Rails 为您处理命名和索引。Rails 将调用此连接表listings_regions而不是regionalizations,但您永远不需要引用它。

以下是您的模型使用简单连接表的外观:

# app/models/listing.rb
class Listing < ApplicationRecord
  has_and_belongs_to_many :regions
  
  accepts_nested_attributes_for :regions, :dependent_destroy
end

# app/models/region.rb
class Region < ApplicationRecord
  has_and_belongs_to_many :listings
end

# app/models/regionalization.rb can be deleted
于 2020-10-30T17:38:21.573 回答